mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-11-08 11:53:47 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 263ff70422 |
@@ -166,3 +166,5 @@ class CarControlSP:
|
||||
@auto_dataclass
|
||||
class CarStateSP:
|
||||
speedLimit: float = auto_field()
|
||||
engineOff: bool = auto_field()
|
||||
engineRpm: float = auto_field()
|
||||
|
||||
@@ -11,14 +11,17 @@ from opendbc.car.toyota import toyotacan
|
||||
from opendbc.car.toyota.values import CAR, NO_STOP_TIMER_CAR, TSS2_CAR, \
|
||||
CarControllerParams, ToyotaFlags
|
||||
from opendbc.can import CANPacker
|
||||
|
||||
from opendbc.sunnypilot.car.toyota.auto_brake_hold import AutoBrakeHoldCarController
|
||||
from opendbc.sunnypilot.car.toyota.enhanced_bsm import EnhancedBsmCarController
|
||||
from opendbc.sunnypilot.car.toyota.gas_interceptor import GasInterceptorCarController
|
||||
from opendbc.sunnypilot.car.toyota.values import ToyotaFlagsSP
|
||||
from opendbc.sunnypilot.car.toyota.brake_onset import BrakeOnsetShaper, BrakeCommandCorrections, EngageOnsetShaper
|
||||
|
||||
Ecu = structs.CarParams.Ecu
|
||||
LongCtrlState = structs.CarControl.Actuators.LongControlState
|
||||
SteerControlType = structs.CarParams.SteerControlType
|
||||
VisualAlert = structs.CarControl.HUDControl.VisualAlert
|
||||
GearShifter = structs.CarState.GearShifter
|
||||
|
||||
# The up limit allows the brakes/gas to unwind quickly leaving a stop,
|
||||
# the down limit roughly matches the rate of ACCEL_NET, reducing PCM compensation windup
|
||||
@@ -36,11 +39,20 @@ MAX_STEER_RATE_FRAMES = 17 # tx control frames needed before torque can be cut
|
||||
# EPS allows user torque above threshold for 50 frames before permanently faulting
|
||||
MAX_USER_TORQUE = 500
|
||||
|
||||
CRUISE_CANCEL_DELAY_FRAMES = 10
|
||||
|
||||
def get_long_tune(CP, params):
|
||||
def get_long_tune(CP, CP_SP, params):
|
||||
if CP.flags & ToyotaFlags.TSS2:
|
||||
kiBP = [2., 5.]
|
||||
kiV = [0.5, 0.25]
|
||||
if CP_SP.flags & ToyotaFlagsSP.TSS2_LONG_TUNING:
|
||||
#kiBP = [0., 2.0, 9.0, 14., 20., 27.]
|
||||
#kiV = [0.25, 0.25, 0.15, 0.12, 0.12, 0.12]
|
||||
#kiBP= [0., 1.0, 2.0, 3.0, 4.0, 5.0, 7., 20., 27., 36.]
|
||||
#kiV = [0.31, 0.32, 0.301, 0.280, 0.259, 0.226, 0.15, 0.15, 0.101, 0.10]
|
||||
kiBP= [0., 0.3, 5.0, 12., 27., 36.]
|
||||
kiV = [0.50, 0.52, 0.25, 0.23, 0.10, 0.09]
|
||||
else:
|
||||
kiBP = [2., 5.]
|
||||
kiV = [0.5, 0.25]
|
||||
else:
|
||||
kiBP = [0., 5., 35.]
|
||||
kiV = [3.6, 2.4, 1.5]
|
||||
@@ -63,15 +75,21 @@ class CarController(CarControllerBase, GasInterceptorCarController):
|
||||
self.permit_braking = True
|
||||
self.steer_rate_counter = 0
|
||||
self.distance_button = 0
|
||||
self.cancel_counter = 0
|
||||
|
||||
# *** start long control state ***
|
||||
self.long_pid = get_long_tune(self.CP, self.params)
|
||||
self.aego = FirstOrderFilter(0.0, 0.25, DT_CTRL * 3)
|
||||
self.long_pid = get_long_tune(self.CP, self.CP_SP, self.params)
|
||||
self.aego = FirstOrderFilter(0.0, 0.45, DT_CTRL * 3)
|
||||
self.pitch = FirstOrderFilter(0, 0.5, DT_CTRL)
|
||||
self.pitch_hp = HighPassFilter(0.0, 0.25, 1.5, DT_CTRL)
|
||||
|
||||
self.accel = 0
|
||||
self.prev_accel = 0
|
||||
# sunnypilot: ToyotaBrakeOnset gates the whole brake_onset module (onset + engage onset + command corrections)
|
||||
self.brake_onset_enabled = bool(CP_SP.flags & ToyotaFlagsSP.SP_BRAKE_ONSET.value)
|
||||
self.brake_onset = BrakeOnsetShaper(DT_CTRL * 3, -ACCEL_WINDDOWN_LIMIT / (DT_CTRL * 3))
|
||||
self.engage_onset = EngageOnsetShaper(DT_CTRL * 3, ACCEL_WINDUP_LIMIT / (DT_CTRL * 3))
|
||||
self.brake_corrections = BrakeCommandCorrections(DT_CTRL * 3) # sunnypilot: see brake_onset.py
|
||||
# *** end long control state ***
|
||||
|
||||
self.packer = CANPacker(dbc_names[Bus.pt])
|
||||
@@ -81,11 +99,20 @@ class CarController(CarControllerBase, GasInterceptorCarController):
|
||||
self.secoc_acc_message_counter = 0
|
||||
self.secoc_prev_reset_counter = 0
|
||||
|
||||
self.enhanced_bsm = EnhancedBsmCarController(CP, CP_SP)
|
||||
self.auto_brake_hold = AutoBrakeHoldCarController(CP, CP_SP)
|
||||
|
||||
self._auto_lock_speed = 0.0
|
||||
|
||||
self._auto_lock_once = False
|
||||
self._gear_prev = GearShifter.park
|
||||
|
||||
def update(self, CC, CC_SP, CS, now_nanos):
|
||||
actuators = CC.actuators
|
||||
stopping = actuators.longControlState == LongCtrlState.stopping
|
||||
hud_control = CC.hudControl
|
||||
pcm_cancel_cmd = CC.cruiseControl.cancel
|
||||
self.cancel_counter = self.cancel_counter + 1 if CC.cruiseControl.cancel else 0
|
||||
pcm_cancel_cmd = self.cancel_counter > CRUISE_CANCEL_DELAY_FRAMES
|
||||
lat_active = CC.latActive and abs(CS.out.steeringTorque) < MAX_USER_TORQUE
|
||||
|
||||
if len(CC.orientationNED) == 3:
|
||||
@@ -196,6 +223,9 @@ class CarController(CarControllerBase, GasInterceptorCarController):
|
||||
|
||||
self.last_standstill = CS.out.standstill
|
||||
|
||||
if self.auto_brake_hold.enabled:
|
||||
can_sends.extend(self.auto_brake_hold.update(CS, self.frame, self.packer))
|
||||
|
||||
# handle UI messages
|
||||
fcw_alert = hud_control.visualAlert == VisualAlert.fcw
|
||||
steer_alert = hud_control.visualAlert in (VisualAlert.steerRequired, VisualAlert.ldw)
|
||||
@@ -213,8 +243,22 @@ class CarController(CarControllerBase, GasInterceptorCarController):
|
||||
|
||||
# internal PCM gas command can get stuck unwinding from negative accel so we apply a generous rate limit
|
||||
pcm_accel_cmd = actuators.accel
|
||||
if CC.longActive:
|
||||
pcm_accel_cmd = rate_limit(pcm_accel_cmd, self.prev_accel, ACCEL_WINDDOWN_LIMIT, ACCEL_WINDUP_LIMIT)
|
||||
if self.brake_onset_enabled:
|
||||
self.engage_onset.cruise_state(CS.out.cruiseState.enabled) # sunnypilot hook
|
||||
if CC.longActive and self.brake_onset_enabled:
|
||||
self.prev_accel = self.engage_onset.start_accel(self.prev_accel, CS.out.aEgo, CS.out.vEgo) # sunnypilot hook
|
||||
# a hard request bypasses the soft brake onset, except in the first 0.6 s after engaging (FCW always does)
|
||||
urgent = self.brake_onset.is_urgent(pcm_accel_cmd, False) and not self.engage_onset.in_engage_window
|
||||
winddown_step = self.brake_onset.down_step(pcm_accel_cmd, self.prev_accel, bypass=fcw_alert, v_ego=CS.out.vEgo,
|
||||
urgent=urgent, t_engaged=self.engage_onset.t_since_engage,
|
||||
a_ego=CS.out.aEgo, brake_force=CS.brake_force)
|
||||
windup_step = self.engage_onset.up_step(True)
|
||||
pcm_accel_cmd = rate_limit(pcm_accel_cmd, self.prev_accel, winddown_step, windup_step)
|
||||
elif CC.longActive:
|
||||
pcm_accel_cmd = rate_limit(pcm_accel_cmd, self.prev_accel, ACCEL_WINDDOWN_LIMIT, ACCEL_WINDUP_LIMIT) # sunnypilot: stock path
|
||||
else:
|
||||
self.brake_onset.reset()
|
||||
self.engage_onset.reset()
|
||||
self.prev_accel = pcm_accel_cmd
|
||||
|
||||
# calculate amount of acceleration PCM should apply to reach target, given pitch.
|
||||
@@ -250,12 +294,19 @@ class CarController(CarControllerBase, GasInterceptorCarController):
|
||||
-MAX_PITCH_COMPENSATION, MAX_PITCH_COMPENSATION))
|
||||
pcm_accel_cmd += pitch_compensation
|
||||
|
||||
# sunnypilot: bleed and freeze the PID integrator at the end of the stop
|
||||
freeze_i = self.brake_onset_enabled and self.brake_corrections.condition_pid(self.long_pid, self.prev_accel, CS.out.vEgo)
|
||||
pcm_accel_cmd = self.long_pid.update(error_future,
|
||||
speed=CS.out.vEgo,
|
||||
feedforward=pcm_accel_cmd,
|
||||
freeze_integrator=actuators.longControlState != LongCtrlState.pid)
|
||||
freeze_integrator=actuators.longControlState != LongCtrlState.pid or freeze_i)
|
||||
if self.brake_onset_enabled:
|
||||
pcm_accel_cmd = self.brake_corrections.apply(pcm_accel_cmd, self.prev_accel, a_ego_future, CS.out.vEgo, stopping,
|
||||
fcw_alert, a_ego=a_ego_blended,
|
||||
relaxed=hud_control.leadDistanceBars == 3) # sunnypilot hook
|
||||
else:
|
||||
self.long_pid.reset()
|
||||
self.brake_corrections.reset() # sunnypilot hook
|
||||
|
||||
# Along with rate limiting positive jerk above, this greatly improves gas response time
|
||||
# Consider the net acceleration request that the PCM should be applying (pitch included)
|
||||
@@ -319,6 +370,9 @@ class CarController(CarControllerBase, GasInterceptorCarController):
|
||||
if self.frame % 20 == 0 and self.CP.flags & ToyotaFlags.DISABLE_RADAR.value:
|
||||
can_sends.append(make_tester_present_msg(0x750, 0, 0xF))
|
||||
|
||||
if self.enhanced_bsm.enabled:
|
||||
can_sends.extend(self.enhanced_bsm.update(CS, self.frame))
|
||||
|
||||
new_actuators = actuators.as_builder()
|
||||
new_actuators.torque = apply_torque / self.params.STEER_MAX
|
||||
new_actuators.torqueOutputCan = apply_torque
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import copy
|
||||
from enum import IntEnum
|
||||
import importlib
|
||||
|
||||
from opendbc.can import CANDefine, CANParser
|
||||
from opendbc.can.dbc import DBC as DBCParser
|
||||
from opendbc.car import Bus, DT_CTRL, create_button_events, structs
|
||||
from opendbc.car.common.conversions import Conversions as CV
|
||||
from opendbc.car.common.filter_simple import FirstOrderFilter
|
||||
@@ -8,11 +11,34 @@ from opendbc.car.interfaces import CarStateBase
|
||||
from opendbc.car.toyota.values import ToyotaFlags, CAR, DBC, STEER_THRESHOLD, NO_STOP_TIMER_CAR, \
|
||||
TSS2_CAR, EPS_SCALE
|
||||
from opendbc.sunnypilot.car.toyota.carstate_ext import CarStateExt
|
||||
from opendbc.sunnypilot.car.toyota.enhanced_bsm import EnhancedBsmCarState
|
||||
from opendbc.sunnypilot.car.toyota.values import ToyotaFlagsSP
|
||||
|
||||
ButtonType = structs.CarState.ButtonEvent.Type
|
||||
SteerControlType = structs.CarParams.SteerControlType
|
||||
|
||||
|
||||
class AccelPersonality(IntEnum):
|
||||
eco = 0
|
||||
normal = 1
|
||||
sport = 2
|
||||
|
||||
|
||||
def get_accel_personality(sport_mode: int, eco_mode: int) -> AccelPersonality:
|
||||
if sport_mode == 1:
|
||||
return AccelPersonality.sport
|
||||
if eco_mode == 1:
|
||||
return AccelPersonality.eco
|
||||
return AccelPersonality.normal
|
||||
|
||||
|
||||
def get_host_params():
|
||||
"""Return sunnypilot's Params store when opendbc is embedded in openpilot."""
|
||||
try:
|
||||
return importlib.import_module("openpilot.common.params").Params()
|
||||
except ModuleNotFoundError:
|
||||
return None
|
||||
|
||||
# These steering fault definitions seem to be common across LKA (torque) and LTA (angle):
|
||||
# - high steer rate fault: goes to 21 or 25 for 1 frame, then 9 for 2 seconds
|
||||
# - lka/lta msg drop out: goes to 9 then 11 for a combined total of 2 seconds, then 3.
|
||||
@@ -55,6 +81,21 @@ class CarState(CarStateBase, CarStateExt):
|
||||
self.gvc = 0.0
|
||||
self.secoc_synchronization = None
|
||||
|
||||
self.enhanced_bsm = EnhancedBsmCarState(CP, CP_SP)
|
||||
|
||||
if CP_SP.flags & ToyotaFlagsSP.SP_AUTO_BRAKE_HOLD:
|
||||
self.pre_collision_2 = {}
|
||||
self.brake_force = float('nan')
|
||||
self._host_params = get_host_params()
|
||||
self.toyota_drive_mode = self._host_params is not None and self._host_params.get_bool('ToyotaDriveMode')
|
||||
self._drive_mode_signals_checked = False
|
||||
self._sport_signal_available = False
|
||||
self._eco_signal_available = False
|
||||
self._prev_accel_profile = None
|
||||
self._accel_profile_init = False
|
||||
|
||||
self.frame = 0
|
||||
|
||||
def update(self, can_parsers) -> tuple[structs.CarState, structs.CarStateSP]:
|
||||
cp = can_parsers[Bus.pt]
|
||||
cp_cam = can_parsers[Bus.cam]
|
||||
@@ -72,18 +113,47 @@ class CarState(CarStateBase, CarStateExt):
|
||||
ret.parkingBrake = cp.vl["BODY_CONTROL_STATE"]["PARKING_BRAKE"] == 1
|
||||
|
||||
ret.brakePressed = cp.vl["BRAKE_MODULE"]["BRAKE_PRESSED"] != 0
|
||||
ret.brakeHoldActive = cp.vl["ESP_CONTROL"]["BRAKE_HOLD_ACTIVE"] == 1
|
||||
#ret.brakeHoldActive = cp.vl["ESP_CONTROL"]["BRAKE_HOLD_ACTIVE"] == 1
|
||||
|
||||
if self.CP.flags & ToyotaFlags.SECOC.value:
|
||||
self.secoc_synchronization = copy.copy(cp.vl["SECOC_SYNCHRONIZATION"])
|
||||
ret.gasPressed = cp.vl["GAS_PEDAL"]["GAS_PEDAL_USER"] > 0
|
||||
can_gear = int(cp.vl["GEAR_PACKET_HYBRID"]["GEAR"])
|
||||
else:
|
||||
ret.gasPressed = cp.vl["PCM_CRUISE"]["GAS_RELEASED"] == 0
|
||||
ret.gasPressed = cp.vl["PCM_CRUISE"]["GAS_RELEASED"] == 0 # TODO: these also have GAS_PEDAL, come back and unify
|
||||
can_gear = int(cp.vl["GEAR_PACKET"]["GEAR"])
|
||||
if not self.CP.flags & ToyotaFlags.DISABLE_RADAR.value:
|
||||
ret.stockAeb = bool(cp_acc.vl["PRE_COLLISION"]["PRECOLLISION_ACTIVE"] and cp_acc.vl["PRE_COLLISION"]["FORCE"] < -1e-5)
|
||||
|
||||
if self.toyota_drive_mode: # and not self.CP.flags & ToyotaFlags.SECOC.value:
|
||||
sport_signal = 'SPORT_ON_2' if self.CP.carFingerprint in (CAR.TOYOTA_RAV4_TSS2, CAR.LEXUS_ES_TSS2,
|
||||
CAR.TOYOTA_HIGHLANDER_TSS2) else 'SPORT_ON'
|
||||
|
||||
if not self._drive_mode_signals_checked:
|
||||
self._drive_mode_signals_checked = True
|
||||
try:
|
||||
sport_mode = cp.vl["GEAR_PACKET"][sport_signal]
|
||||
self._sport_signal_available = True
|
||||
except KeyError:
|
||||
sport_mode = 0
|
||||
self._sport_signal_available = False
|
||||
try:
|
||||
eco_mode = cp.vl["GEAR_PACKET"]['ECON_ON']
|
||||
self._eco_signal_available = True
|
||||
except KeyError:
|
||||
eco_mode = 0
|
||||
self._eco_signal_available = False
|
||||
else:
|
||||
sport_mode = cp.vl["GEAR_PACKET"][sport_signal] if self._sport_signal_available else 0
|
||||
eco_mode = cp.vl["GEAR_PACKET"]['ECON_ON'] if self._eco_signal_available else 0
|
||||
|
||||
accel_profile = get_accel_personality(sport_mode, eco_mode)
|
||||
|
||||
if not self._accel_profile_init or accel_profile != self._prev_accel_profile:
|
||||
self._host_params.put('AccelPersonality', int(accel_profile))
|
||||
self._accel_profile_init = True
|
||||
self._prev_accel_profile = accel_profile
|
||||
|
||||
self.parse_wheel_speeds(ret,
|
||||
cp.vl["WHEEL_SPEEDS"]["WHEEL_SPEED_FL"],
|
||||
cp.vl["WHEEL_SPEEDS"]["WHEEL_SPEED_FR"],
|
||||
@@ -212,6 +282,16 @@ class CarState(CarStateBase, CarStateExt):
|
||||
|
||||
ret.buttonEvents = buttonEvents
|
||||
|
||||
if self.enhanced_bsm.enabled and self.frame > 199:
|
||||
ret.leftBlindspot, ret.rightBlindspot = self.enhanced_bsm.update(cp, self.frame)
|
||||
|
||||
if self.CP_SP.flags & ToyotaFlagsSP.SP_AUTO_BRAKE_HOLD:
|
||||
self.pre_collision_2 = copy.copy(cp_cam.vl["PRE_COLLISION_2"])
|
||||
if "BRAKE" in cp.vl:
|
||||
self.brake_force = float(cp.vl["BRAKE"]["BRAKE_FORCE"])
|
||||
|
||||
self.frame += 1
|
||||
|
||||
CarStateExt.update(self, ret, ret_sp, can_parsers)
|
||||
|
||||
return ret, ret_sp
|
||||
@@ -221,6 +301,11 @@ class CarState(CarStateBase, CarStateExt):
|
||||
pt_messages = [
|
||||
("BLINKERS_STATE", float('nan')),
|
||||
]
|
||||
if CP.flags & ToyotaFlags.HYBRID:
|
||||
pt_messages.append(("ENGINE_RPM", float('nan')))
|
||||
|
||||
if CP_SP.flags & ToyotaFlagsSP.SP_AUTO_BRAKE_HOLD and "BRAKE" in DBCParser(DBC[CP.carFingerprint][Bus.pt]).name_to_msg:
|
||||
pt_messages.append(("BRAKE", float('nan')))
|
||||
|
||||
cam_messages = [
|
||||
("RSA1", 0),
|
||||
|
||||
@@ -3,7 +3,7 @@ from opendbc.car.toyota.carstate import CarState
|
||||
from opendbc.car.toyota.carcontroller import CarController
|
||||
from opendbc.car.toyota.radar_interface import RadarInterface
|
||||
from opendbc.car.toyota.values import Ecu, CAR, DBC, ToyotaFlags, CarControllerParams, TSS2_CAR, RADAR_ACC_CAR, NO_DSU_CAR, \
|
||||
MIN_ACC_SPEED, EPS_SCALE, NO_STOP_TIMER_CAR, ToyotaSafetyFlags, UNSUPPORTED_DSU_CAR
|
||||
MIN_ACC_SPEED, EPS_SCALE, NO_STOP_TIMER_CAR, ToyotaSafetyFlags, UNSUPPORTED_DSU_CAR, SECOC_CAR
|
||||
from opendbc.car.disable_ecu import disable_ecu
|
||||
from opendbc.car.interfaces import CarInterfaceBase
|
||||
from opendbc.sunnypilot.car.toyota.values import ToyotaFlagsSP, ToyotaSafetyFlagsSP
|
||||
@@ -117,6 +117,7 @@ class CarInterface(CarInterfaceBase):
|
||||
|
||||
if ret.flags & ToyotaFlags.TSS2:
|
||||
ret.flags |= ToyotaFlags.RAISED_ACCEL_LIMIT.value
|
||||
#ret.stopAccel = -0.02
|
||||
|
||||
# Hybrids have much quicker longitudinal actuator response
|
||||
if ret.flags & ToyotaFlags.HYBRID.value:
|
||||
@@ -130,6 +131,9 @@ class CarInterface(CarInterfaceBase):
|
||||
if candidate in UNSUPPORTED_DSU_CAR:
|
||||
ret.safetyParam |= ToyotaSafetyFlagsSP.UNSUPPORTED_DSU
|
||||
|
||||
if candidate == CAR.TOYOTA_PRIUS_TSS2:
|
||||
ret.flags |= ToyotaFlagsSP.SP_NEED_DEBUG_BSM.value
|
||||
|
||||
# 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):
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from opendbc.car.structs import CarParams
|
||||
from opendbc.car.can_definitions import CanData
|
||||
|
||||
SteerControlType = CarParams.SteerControlType
|
||||
|
||||
@@ -164,3 +165,47 @@ def toyota_checksum(address: int, sig, d: bytearray) -> int:
|
||||
for i in range(len(d) - 1):
|
||||
s += d[i]
|
||||
return s & 0xFF
|
||||
|
||||
def create_set_bsm_debug_mode(lr_blindspot, enabled):
|
||||
dat = b"\x02\x10\x60\x00\x00\x00\x00" if enabled else b"\x02\x10\x01\x00\x00\x00\x00"
|
||||
dat = lr_blindspot + dat
|
||||
|
||||
return CanData(0x750, dat, 0)
|
||||
|
||||
|
||||
def create_bsm_polling_status(lr_blindspot):
|
||||
return CanData(0x750, lr_blindspot + b"\x02\x21\x69\x00\x00\x00\x00", 0)
|
||||
|
||||
|
||||
# auto brake hold
|
||||
def create_brake_hold_command(packer, frame, pre_collision_2, brake_hold_active):
|
||||
# forward PRE_COLLISION_2 when auto brake hold is not active
|
||||
values = {s: pre_collision_2[s] for s in [
|
||||
"DSS1GDRV",
|
||||
"DS1STAT2",
|
||||
"DS1STBK2",
|
||||
"PCSWAR",
|
||||
"PCSALM",
|
||||
"PCSOPR",
|
||||
"PCSABK",
|
||||
"PBATRGR",
|
||||
"PPTRGR",
|
||||
"IBTRGR",
|
||||
"CLEXTRGR",
|
||||
"IRLT_REQ",
|
||||
"BRKHLD",
|
||||
"AVSTRGR",
|
||||
"VGRSTRGR",
|
||||
"PREFILL",
|
||||
"PBRTRGR",
|
||||
"PCSDIS",
|
||||
"PBPREPMP",
|
||||
]}
|
||||
|
||||
if brake_hold_active:
|
||||
values = {
|
||||
"DSS1GDRV": 0x3FF,
|
||||
"PBRTRGR": frame % 730 < 727, # cut actuation for 3 frames
|
||||
}
|
||||
|
||||
return packer.make_can_msg("PRE_COLLISION_2", 0, values)
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
BO_ 1880 DEBUG: 8 XXX
|
||||
SG_ BLINDSPOTSIDE : 7|8@0+ (1,0) [0|255] "" XXX
|
||||
SG_ BLINDSPOT : 38|1@0+ (1,0) [0|15] "" XXX
|
||||
SG_ BLINDSPOTD1 : 47|8@0+ (1,0) [0|255] "" XXX
|
||||
SG_ BLINDSPOTD2 : 55|8@0+ (1,0) [0|255] "" XXX
|
||||
|
||||
BO_ 836 PRE_COLLISION_2: 8 DSU
|
||||
SG_ DSS1GDRV : 7|10@0- (0.1,0) [0|0] "m/s^2" Vector__XXX
|
||||
SG_ DS1STAT2 : 13|3@0+ (1,0) [0|0] "" Vector__XXX
|
||||
SG_ DS1STBK2 : 10|3@0+ (1,0) [0|0] "" Vector__XXX
|
||||
SG_ PCSWAR : 18|1@0+ (1,0) [0|0] "" FCM
|
||||
SG_ PCSALM : 17|1@0+ (1,0) [0|0] "" FCM
|
||||
SG_ PCSOPR : 16|1@0+ (1,0) [0|0] "" Vector__XXX
|
||||
SG_ PCSABK : 31|1@0+ (1,0) [0|0] "" Vector__XXX
|
||||
SG_ PBATRGR : 30|2@0+ (1,0) [0|0] "" Vector__XXX
|
||||
SG_ PPTRGR : 28|1@0+ (1,0) [0|0] "" FCM
|
||||
SG_ IBTRGR : 27|1@0+ (1,0) [0|0] "" FCM
|
||||
SG_ CLEXTRGR : 26|1@0+ (1,0) [0|0] "" Vector__XXX
|
||||
SG_ IRLT_REQ : 25|2@0+ (1,0) [0|0] "" Vector__XXX
|
||||
SG_ BRKHLD : 37|1@0+ (1,0) [0|0] "" Vector__XXX
|
||||
SG_ AVSTRGR : 36|1@0+ (1,0) [0|0] "" SCS
|
||||
SG_ VGRSTRGR : 35|2@0+ (1,0) [0|0] "" Vector__XXX
|
||||
SG_ PREFILL : 33|1@0+ (1,0) [0|0] "" Vector__XXX
|
||||
SG_ PBRTRGR : 32|1@0+ (1,0) [0|0] "" Vector__XXX
|
||||
SG_ PCSDIS : 43|1@0+ (1,0) [0|0] "" Vector__XXX
|
||||
SG_ PBPREPMP : 40|1@0+ (1,0) [0|0] "" Vector__XXX
|
||||
SG_ CHECKSUM : 63|8@0+ (1,0) [0|0] "" XXX
|
||||
|
||||
BO_ 713 HYBRID_POWERTRAIN: 8 XXX
|
||||
SG_ COAST_FUEL_CUT : 5|1@0+ (1,0) [0|1] "" XXX
|
||||
SG_ HYBRID_DRIVE_STATE : 11|4@0+ (1,0) [0|15] "" XXX
|
||||
|
||||
BO_ 814 BRAKE_HYDRAULIC: 8 XXX
|
||||
SG_ BRAKE_PRESSURE : 15|8@0+ (1,0) [0|255] "" XXX
|
||||
SG_ BRAKE_PRESSURE_COPY1 : 31|8@0+ (1,0) [0|255] "" XXX
|
||||
SG_ BRAKE_PRESSURE_COPY2 : 47|8@0+ (1,0) [0|255] "" XXX
|
||||
|
||||
BO_ 971 MOTOR_SPEED_CLUSTER: 7 XXX
|
||||
SG_ CLUSTER_SPEED : 55|8@0+ (1,-21) [0|255] "km/h" XXX
|
||||
|
||||
BO_ 896 HYBRID_BATTERY: 8 XXX
|
||||
SG_ HV_SOC_PCT : 55|8@0+ (1,0) [0|100] "%" XXX
|
||||
|
||||
BO_ 1654 HV_POWER_INTEGRATOR: 8 XXX
|
||||
SG_ HV_POWER_ACCUM : 39|8@0+ (1,0) [0|255] "" XXX
|
||||
|
||||
BO_ 975 HV_INVERTER: 5 XXX
|
||||
SG_ HV_VOLTAGE_LO : 23|8@0+ (1,0) [0|255] "" XXX
|
||||
SG_ HV_CURRENT_LO : 39|8@0+ (1,0) [0|255] "" XXX
|
||||
|
||||
BO_ 955 BRAKE_REDUNDANT: 8 XXX
|
||||
SG_ BRAKE_PRESSED_REDUNDANT : 0|1@0+ (1,0) [0|1] "" XXX
|
||||
|
||||
BO_ 918 VEHICLE_DRIVE_MODE: 8 XXX
|
||||
SG_ DRIVE_MODE_STATE : 7|8@0+ (1,0) [0|255] "" XXX
|
||||
|
||||
CM_ "Sunnypilot Prius TSS2 reverse-engineered signals — route 550a71ee4c7a7fbe/0000038f--6b48497966, 2026-05-19. Confidence: high unless noted.";
|
||||
CM_ BO_ 713 "Hybrid powertrain status. 100Hz on bus 0.";
|
||||
CM_ SG_ 713 COAST_FUEL_CUT "byte0 bit5. 1 when accelerator lifted at speed -> ICE fuel cut / decel-fuel-cut active. P(=1|coast)=0.95, P(=1|accel)=0.008, P(=1|brake)=0.0. Use: lift detection for accel feedforward, EV-only mode hint.";
|
||||
CM_ SG_ 713 HYBRID_DRIVE_STATE "byte1 lower-nibble (bits 8-11). Enum (validated): 4=brake/regen-active, 5-8=coast/cruise levels, 10/12=ICE running active, 11=idle-stop. Not a scalar magnitude. Use: distinguish ICE-on vs EV for accel command shaping.";
|
||||
CM_ BO_ 814 "Brake hydraulic. 5Hz on bus 0. Bytes 0,2,4,6 are status/flag bytes (not yet decoded). Saturates 255 under hard brake.";
|
||||
CM_ SG_ 814 BRAKE_PRESSURE "byte1. Master cylinder pressure proxy. 0 during coast, 33-45 mean during brake, 254-255 max. NOT same as BRAKE_MODULE.BRAKE_PRESSURE.";
|
||||
CM_ SG_ 814 BRAKE_PRESSURE_COPY1 "byte3. Identical to BRAKE_PRESSURE 90% of brake samples. Redundant copy (CRC/safety).";
|
||||
CM_ SG_ 814 BRAKE_PRESSURE_COPY2 "byte5. Identical to BRAKE_PRESSURE 90% of brake samples. Redundant copy.";
|
||||
CM_ BO_ 971 "Cluster speedometer feed. 10Hz on bus 0.";
|
||||
CM_ SG_ 971 CLUSTER_SPEED "byte6. Encoded as raw_byte = v_kph + 21. After (1,-21) factor/offset: km/h matching dashboard speedometer. Corr 0.998 w/ WHEEL_SPEEDS. ~5km/h higher than wheel mean = cluster display bias.";
|
||||
CM_ BO_ 896 "HV battery status. 5Hz on bus 0.";
|
||||
CM_ SG_ 896 HV_SOC_PCT "byte6. HV battery state-of-charge percent. Validated range 53-62% on route 0000038f (Prius hybrid SoC normal band 40-80%). Use: energy-aware planner — reduce regen demand at high SoC (>70%), lift accel demand at low SoC (<50%).";
|
||||
CM_ BO_ 1654 "HV power integrator. 1Hz on bus 0. Slow drift signal, semantic not fully confirmed.";
|
||||
CM_ SG_ 1654 HV_POWER_ACCUM "byte4. Slow-drifting integer 0-59, independent of HV_SOC_PCT. Hypothesis: HV power flow accumulator or charging-cycle counter. Needs more routes.";
|
||||
CM_ BO_ 975 "HV inverter telemetry. 10Hz on bus 0. 5-byte message.";
|
||||
CM_ SG_ 975 HV_VOLTAGE_LO "byte2. Sequential integer 40-53 range. Hypothesis: HV bus voltage low-byte scaled. Moderate corr w/ brake (regen-loaded voltage).";
|
||||
CM_ SG_ 975 HV_CURRENT_LO "byte4. Sequential integer 17-36 range. JUMPS -16 at brake-release (regen current cutoff signature). Use: real-time regen current readback for brake-blend tuning.";
|
||||
CM_ BO_ 955 "Brake redundant signal. 10Hz on bus 0.";
|
||||
CM_ SG_ 955 BRAKE_PRESSED_REDUNDANT "byte0 bit0. 99.9% agreement with BRAKE_MODULE.BRAKE_PRESSED. Safety-redundant copy. Use: cross-check brake state for fault detection.";
|
||||
CM_ BO_ 918 "Vehicle drive mode state. 5Hz on bus 0.";
|
||||
CM_ SG_ 918 DRIVE_MODE_STATE "byte0. 3 states observed: 185=parked-with-brake, 189=ready-to-drive, 191=ready-no-brake. Transitions on GEAR shifts + brake. Use: more reliable drive-state machine than gear+brake combo.";
|
||||
VAL_ 713 HYBRID_DRIVE_STATE 4 "brake_regen_active" 5 "coast_5" 6 "coast_6" 7 "coast_7" 8 "coast_cruise" 10 "ice_active_10" 11 "idle_stop" 12 "ice_active_12";
|
||||
VAL_ 918 DRIVE_MODE_STATE 185 "parked_brake" 189 "ready" 191 "ready_no_brake";
|
||||
@@ -234,6 +234,7 @@ BO_ 956 GEAR_PACKET: 8 XXX
|
||||
SG_ SPORT_GEAR_ON : 33|1@0+ (1,0) [0|1] "" XXX
|
||||
SG_ SPORT_GEAR : 38|3@0+ (1,0) [0|7] "" XXX
|
||||
SG_ ECON_ON : 40|1@0+ (1,0) [0|1] "" XXX
|
||||
SG_ SPORT_ON_2 : 55|1@0+ (1,0) [0|1] "" XXX
|
||||
SG_ B_GEAR_ENGAGED : 41|1@0+ (1,0) [0|1] "" XXX
|
||||
SG_ DRIVE_ENGAGED : 47|1@0+ (1,0) [0|1] "" XXX
|
||||
|
||||
@@ -545,6 +546,7 @@ VAL_ 956 GEAR 0 "D" 1 "S" 8 "N" 16 "R" 32 "P";
|
||||
VAL_ 956 SPORT_GEAR_ON 0 "off" 1 "on";
|
||||
VAL_ 956 SPORT_GEAR 1 "S1" 2 "S2" 3 "S3" 4 "S4" 5 "S5" 6 "S6";
|
||||
VAL_ 956 ECON_ON 0 "off" 1 "on";
|
||||
VAL_ 956 SPORT_ON_2 0 "off" 1 "on";
|
||||
VAL_ 956 B_GEAR_ENGAGED 0 "off" 1 "on";
|
||||
VAL_ 956 DRIVE_ENGAGED 0 "off" 1 "on";
|
||||
VAL_ 1005 REVERSE_CAMERA_GUIDELINES 3 "No guidelines" 2 "Static guidelines" 1 "Active guidelines";
|
||||
|
||||
@@ -35,14 +35,6 @@ BO_ 740 STEERING_LKA: 5 XXX
|
||||
SG_ STEER_TORQUE_CMD : 15|16@0- (1,0) [0|65535] "" XXX
|
||||
SG_ CHECKSUM : 39|8@0+ (1,0) [0|255] "" XXX
|
||||
|
||||
BO_ 836 PRE_COLLISION_2: 8 DSU
|
||||
SG_ DSS1GDRV : 7|10@0- (0.1,0) [0|0] "m/s^2" Vector__XXX
|
||||
SG_ PCSALM : 17|1@0+ (1,0) [0|0] "" FCM
|
||||
SG_ IBTRGR : 27|1@0+ (1,0) [0|0] "" FCM
|
||||
SG_ PBATRGR : 30|2@0+ (1,0) [0|0] "" Vector__XXX
|
||||
SG_ PREFILL : 33|1@0+ (1,0) [0|0] "" Vector__XXX
|
||||
SG_ AVSTRGR : 36|1@0+ (1,0) [0|0] "" SCS
|
||||
SG_ CHECKSUM : 63|8@0+ (1,0) [0|0] "" XXX
|
||||
|
||||
CM_ SG_ 466 NEUTRAL_FORCE "force in newtons the engine/electric motors are applying without any acceleration commands or user input";
|
||||
CM_ SG_ 466 ACC_BRAKING "whether brakes are being actuated from ACC command";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
CM_ "IMPORT _community.dbc";
|
||||
CM_ "IMPORT _toyota_2017.dbc";
|
||||
CM_ "IMPORT _toyota_adas_standard.dbc";
|
||||
CM_ "IMPORT _sp_debug_toyota.dbc";
|
||||
|
||||
BO_ 548 BRAKE_MODULE: 8 XXX
|
||||
SG_ BRAKE_PRESSURE : 43|12@0+ (1,0) [0|4047] "" XXX
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
CM_ "IMPORT _community.dbc";
|
||||
CM_ "IMPORT _toyota_2017.dbc";
|
||||
CM_ "IMPORT _toyota_adas_standard.dbc";
|
||||
CM_ "IMPORT _sp_debug_toyota.dbc";
|
||||
|
||||
BO_ 401 STEERING_LTA: 8 XXX
|
||||
SG_ CHECKSUM : 63|8@0+ (1,0) [0|255] "" XXX
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
CM_ "IMPORT _community.dbc";
|
||||
CM_ "IMPORT _toyota_2017.dbc";
|
||||
CM_ "IMPORT _toyota_adas_standard.dbc";
|
||||
CM_ "IMPORT _sp_debug_toyota.dbc";
|
||||
|
||||
BO_ 550 BRAKE_MODULE: 8 XXX
|
||||
SG_ BRAKE_PRESSURE : 0|9@0+ (1,0) [0|511] "" XXX
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
// Stock longitudinal
|
||||
#define TOYOTA_BASE_TX_MSGS \
|
||||
{0x191, 0, 8, .check_relay = true}, {0x412, 0, 8, .check_relay = true}, {0x1D2, 0, 8, .check_relay = false}, /* LKAS + LTA + PCM cancel cmd */ \
|
||||
{0x191, 0, 8, .check_relay = true}, {0x412, 0, 8, .check_relay = true}, {0x1D2, 0, 8, .check_relay = false}, {0x750, 0, 8, .check_relay = false}, /* LKAS + LTA + PCM cancel cmd */ \
|
||||
|
||||
#define TOYOTA_COMMON_TX_MSGS \
|
||||
TOYOTA_BASE_TX_MSGS \
|
||||
@@ -69,6 +69,7 @@ static bool toyota_secoc = false;
|
||||
static bool toyota_alt_brake = false;
|
||||
static bool toyota_stock_longitudinal = false;
|
||||
static bool toyota_lta = false;
|
||||
static bool toyota_cruise_engaged = false; // SP: PCM_CRUISE.CRUISE_ACTIVE, narrows the auto brake hold AEB window below
|
||||
static int toyota_dbc_eps_torque_factor = 100; // conversion factor for STEER_TORQUE_EPS in %: see dbc file
|
||||
|
||||
static uint32_t toyota_compute_checksum(const CANPacket_t *msg) {
|
||||
@@ -151,6 +152,7 @@ static void toyota_rx_hook(const CANPacket_t *msg) {
|
||||
if (msg->addr == 0x176U) {
|
||||
bool cruise_engaged = GET_BIT(msg, 5U); // PCM_CRUISE.CRUISE_ACTIVE
|
||||
pcm_cruise_check(cruise_engaged);
|
||||
toyota_cruise_engaged = cruise_engaged;
|
||||
}
|
||||
if (msg->addr == 0x116U) {
|
||||
gas_pressed = msg->data[1] != 0U; // GAS_PEDAL.GAS_PEDAL_USER
|
||||
@@ -162,6 +164,7 @@ static void toyota_rx_hook(const CANPacket_t *msg) {
|
||||
if (msg->addr == 0x1D2U) {
|
||||
bool cruise_engaged = GET_BIT(msg, 5U); // PCM_CRUISE.CRUISE_ACTIVE
|
||||
pcm_cruise_check(cruise_engaged);
|
||||
toyota_cruise_engaged = cruise_engaged;
|
||||
|
||||
if (!enable_gas_interceptor) {
|
||||
gas_pressed = !GET_BIT(msg, 4U); // PCM_CRUISE.GAS_RELEASED
|
||||
@@ -386,13 +389,35 @@ static bool toyota_tx_hook(const CANPacket_t *msg) {
|
||||
tx = false;
|
||||
}
|
||||
}
|
||||
|
||||
// SP: auto brake hold https://github.com/AlexandreSato
|
||||
if ((msg->addr == 0x344U) && (alternative_experience & ALT_EXP_ALLOW_AEB)) {
|
||||
if (vehicle_moving || gas_pressed || !acc_main_on || toyota_cruise_engaged) {
|
||||
tx = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UDS: Only tester present ("\x0F\x02\x3E\x00\x00\x00\x00\x00") allowed on diagnostics address
|
||||
if (msg->addr == 0x750U) {
|
||||
// this address is sub-addressed. only allow tester present to radar (0xF)
|
||||
bool invalid_uds_msg = (GET_BYTES(msg, 0, 4) != 0x003E020FU) || (GET_BYTES(msg, 4, 4) != 0x0U);
|
||||
if (invalid_uds_msg) {
|
||||
// SP: Secret sauce from dp. (ask @rav4kumar prior to modifying)
|
||||
// Enhanced BSM
|
||||
bool sp_valid_uds_msgs = ((GET_BYTES(msg, 0, 4) == 0x01100241U) || // disable left BSM debug
|
||||
(GET_BYTES(msg, 0, 4) == 0x60100241U) || // enable left BSM debug
|
||||
(GET_BYTES(msg, 0, 4) == 0x69210241U) || // poll left BSM status
|
||||
(GET_BYTES(msg, 0, 4) == 0x01100242U) || // disable right BSM debug
|
||||
(GET_BYTES(msg, 0, 4) == 0x60100242U) || // enable right BSM debug
|
||||
(GET_BYTES(msg, 0, 4) == 0x69210242U)) // poll right BSM status
|
||||
&& (GET_BYTES(msg, 4, 4) == 0x0U);
|
||||
|
||||
sp_valid_uds_msgs |= (GET_BYTES(msg, 0, 4) == 0x11300540U) && // automatic door locking and unlocking
|
||||
((GET_BYTES(msg, 4, 4) == 0x00004000U) || // unlock
|
||||
(GET_BYTES(msg, 4, 4) == 0x00008000U)); // lock
|
||||
|
||||
bool valid_tester_present = !invalid_uds_msg && !toyota_stock_longitudinal && !toyota_secoc;
|
||||
if (!valid_tester_present && !sp_valid_uds_msgs) {
|
||||
tx = false;
|
||||
}
|
||||
}
|
||||
@@ -566,10 +591,27 @@ static safety_config toyota_init(uint16_t param) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
static bool toyota_fwd_hook(int bus_num, int addr) {
|
||||
bool block_msg = false;
|
||||
if (bus_num == 2) {
|
||||
// SP: block AEB when auto brake hold is active, unblock AEB when auto brake hold is not active.
|
||||
// Narrowed to match auto brake hold's own precondition (cruise must be off) - previously this
|
||||
// blocked native AEB forwarding, forcing a slower software relay, any time the car was simply
|
||||
// stopped with the gas released and ACC main on, even while cruise was actively engaged and
|
||||
// auto brake hold couldn't be active at all.
|
||||
bool is_aeb_msg = (addr == 0x344);
|
||||
block_msg = (is_aeb_msg && (alternative_experience & ALT_EXP_ALLOW_AEB) && !vehicle_moving && !gas_pressed && acc_main_on &&
|
||||
!toyota_cruise_engaged);
|
||||
}
|
||||
|
||||
return block_msg;
|
||||
}
|
||||
|
||||
const safety_hooks toyota_hooks = {
|
||||
.init = toyota_init,
|
||||
.rx = toyota_rx_hook,
|
||||
.tx = toyota_tx_hook,
|
||||
.fwd = toyota_fwd_hook,
|
||||
.get_checksum = toyota_get_checksum,
|
||||
.compute_checksum = toyota_compute_checksum,
|
||||
.get_quality_flag_valid = toyota_get_quality_flag_valid,
|
||||
|
||||
@@ -101,6 +101,29 @@ class TestToyotaSafetyBase(common.CarSafetyTest, common.LongitudinalAccelSafetyT
|
||||
tester_present = libsafety_py.make_CANPacket(0x750, 0, msg)
|
||||
self.assertEqual(should_tx and ecu_disabled and not stock_longitudinal, self._tx(tester_present))
|
||||
|
||||
def test_enhanced_bsm(self):
|
||||
# SP: enable/disable/poll left+right blind spot debug mode, sent to the radar diagnostic address
|
||||
valid_msgs = [
|
||||
b"\x41\x02\x10\x60\x00\x00\x00\x00", # enable left
|
||||
b"\x41\x02\x10\x01\x00\x00\x00\x00", # disable left
|
||||
b"\x41\x02\x21\x69\x00\x00\x00\x00", # poll left
|
||||
b"\x42\x02\x10\x60\x00\x00\x00\x00", # enable right
|
||||
b"\x42\x02\x10\x01\x00\x00\x00\x00", # disable right
|
||||
b"\x42\x02\x21\x69\x00\x00\x00\x00", # poll right
|
||||
]
|
||||
for msg in valid_msgs:
|
||||
pkt = libsafety_py.make_CANPacket(0x750, 0, msg)
|
||||
self.assertTrue(self._tx(pkt), msg.hex())
|
||||
|
||||
invalid_msgs = [
|
||||
b"\x41\x02\x10\x61\x00\x00\x00\x00", # wrong subfunction
|
||||
b"\x43\x02\x10\x60\x00\x00\x00\x00", # wrong sub-address (not left/right)
|
||||
b"\x41\x02\x10\x60\x01\x00\x00\x00", # non-zero trailing bytes
|
||||
]
|
||||
for msg in invalid_msgs:
|
||||
pkt = libsafety_py.make_CANPacket(0x750, 0, msg)
|
||||
self.assertFalse(self._tx(pkt), msg.hex())
|
||||
|
||||
def test_block_aeb(self, stock_longitudinal: bool = False):
|
||||
for controls_allowed in (True, False):
|
||||
for bad in (True, False):
|
||||
|
||||
@@ -14,7 +14,7 @@ from opendbc.car import structs
|
||||
from opendbc.car.can_definitions import CanRecvCallable, CanSendCallable
|
||||
from opendbc.car.hyundai.values import HyundaiFlags
|
||||
from opendbc.car.subaru.values import SubaruFlags
|
||||
from opendbc.car.toyota.values import ToyotaSafetyFlags
|
||||
from opendbc.car.toyota.values import RADAR_ACC_CAR, SECOC_CAR, TSS2_CAR, ToyotaSafetyFlags
|
||||
from opendbc.sunnypilot.car.hyundai.enable_radar_tracks import enable_radar_tracks as hyundai_enable_radar_tracks
|
||||
from opendbc.sunnypilot.car.hyundai.longitudinal.helpers import LongitudinalTuningType
|
||||
from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP
|
||||
@@ -157,6 +157,10 @@ def _initialize_toyota(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params
|
||||
if CP.brand == 'toyota':
|
||||
toyota_stock_long = int(params_dict.get("ToyotaEnforceStockLongitudinal", 0)) == 1
|
||||
toyota_stop_and_go_hack = int(params_dict.get("ToyotaStopAndGoHack", 0)) == 1
|
||||
toyota_tss2_long_tuning = int(params_dict.get("ToyotaTSS2Long", 0)) == 1
|
||||
toyota_enhanced_bsm = int(params_dict.get("ToyotaEnhancedBsm", 0)) == 1
|
||||
toyota_auto_brake_hold = int(params_dict.get("ToyotaAutoHold", 0)) == 1
|
||||
toyota_brake_onset = int(params_dict.get("ToyotaBrakeOnset", 0)) == 1
|
||||
|
||||
if toyota_stock_long:
|
||||
CP_SP.flags |= ToyotaFlagsSP.STOCK_LONGITUDINAL.value
|
||||
@@ -166,3 +170,15 @@ def _initialize_toyota(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params
|
||||
|
||||
if toyota_stop_and_go_hack and CP.openpilotLongitudinalControl:
|
||||
CP_SP.flags |= ToyotaFlagsSP.STOP_AND_GO_HACK.value
|
||||
|
||||
if toyota_tss2_long_tuning:
|
||||
CP_SP.flags |= ToyotaFlagsSP.TSS2_LONG_TUNING.value
|
||||
|
||||
if toyota_enhanced_bsm and CP.carFingerprint in (TSS2_CAR - SECOC_CAR):
|
||||
CP_SP.flags |= ToyotaFlagsSP.SP_ENHANCED_BSM.value
|
||||
|
||||
if toyota_auto_brake_hold and CP.carFingerprint in (TSS2_CAR - RADAR_ACC_CAR - SECOC_CAR):
|
||||
CP_SP.flags |= ToyotaFlagsSP.SP_AUTO_BRAKE_HOLD.value
|
||||
|
||||
if toyota_brake_onset:
|
||||
CP_SP.flags |= ToyotaFlagsSP.SP_BRAKE_ONSET.value
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
import math
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.toyota import toyotacan
|
||||
from opendbc.sunnypilot.car.toyota.values import ToyotaFlagsSP
|
||||
|
||||
GearShifter = structs.CarState.GearShifter
|
||||
|
||||
# frames of confirmed hold-eligible standstill required before engaging
|
||||
BRAKE_HOLD_ALLOWED_TIMER = 100
|
||||
BRAKE_HOLD_MIN_FORCE = 1400.0 #n
|
||||
BRAKE_HOLD_LIGHT_TIMER = 250
|
||||
|
||||
DISALLOWED_GEARS = (GearShifter.park, GearShifter.reverse)
|
||||
|
||||
# PRE_COLLISION_2 fields that go high when the camera's own PCS/AEB is genuinely intervening this
|
||||
# frame (PCSALM mirrors PRECOLLISION_ACTIVE; IBTRGR/PBATRGR/PREFILL/AVSTRGR/PBRTRGR/PPTRGR are its
|
||||
# actuation triggers - see create_pcs_commands for the same field set on the stock-DSU PCS path).
|
||||
# Deliberately over-inclusive: a false positive here just means we pass a quiescent frame through
|
||||
# instead of holding it, never the other way around, so err toward checking more fields, not fewer.
|
||||
PCS_TRIGGER_FIELDS = ("PCSALM", "IBTRGR", "PBATRGR", "PREFILL", "AVSTRGR", "PBRTRGR", "PPTRGR")
|
||||
|
||||
|
||||
def pcs_is_active(pre_collision_2: dict) -> bool:
|
||||
return any(pre_collision_2.get(field, 0) for field in PCS_TRIGGER_FIELDS) or pre_collision_2.get("DSS1GDRV", 0) != 0
|
||||
|
||||
|
||||
class AutoBrakeHold:
|
||||
def __init__(self, CP: structs.CarParams, CP_SP: structs.CarParamsSP):
|
||||
self.CP = CP
|
||||
self.CP_SP = CP_SP
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
return bool(self.CP_SP.flags & ToyotaFlagsSP.SP_AUTO_BRAKE_HOLD)
|
||||
|
||||
|
||||
# Auto Brake Hold (@AlexandreSato, @rav4kumar): holds the car at a stop with cruise off by
|
||||
# overriding PRE_COLLISION_2 - the only channel on this platform that can command the brake
|
||||
# independent of ACC engagement, since PCS/AEB is an always-on active safety system by design.
|
||||
# Yields to any genuine PCS activation this frame - the real message is only ever overridden while
|
||||
# it's quiescent - and releases for the rest of the current standstill episode on a brake press,
|
||||
# rather than for a single frame.
|
||||
class AutoBrakeHoldCarController(AutoBrakeHold):
|
||||
def __init__(self, CP: structs.CarParams, CP_SP: structs.CarParamsSP):
|
||||
super().__init__(CP, CP_SP)
|
||||
|
||||
self.active = False
|
||||
self._counter = 0
|
||||
self._released = False
|
||||
self._armed = False
|
||||
self._firm_frame = 0
|
||||
self._prev_brake_pressed = False
|
||||
|
||||
def update(self, CS: structs.CarState, frame: int, packer) -> list:
|
||||
relay_blocked = (CS.out.standstill and CS.out.cruiseState.available and not CS.out.cruiseState.enabled and
|
||||
not CS.out.gasPressed)
|
||||
hold_allowed = relay_blocked and CS.out.gearShifter not in DISALLOWED_GEARS
|
||||
|
||||
if hold_allowed:
|
||||
# only a fresh press releases hold - the press that caused the stop is already reflected in
|
||||
# _prev_brake_pressed by the time standstill is reached, so it doesn't count as a release
|
||||
if CS.out.brakePressed and not self._prev_brake_pressed:
|
||||
self._released = True
|
||||
force = getattr(CS, "brake_force", float("nan"))
|
||||
self._counter += 1
|
||||
if not self._armed and (math.isnan(force) or force >= BRAKE_HOLD_MIN_FORCE):
|
||||
self._armed = True
|
||||
self._firm_frame = self._counter
|
||||
firm_ready = self._armed and self._counter - self._firm_frame >= BRAKE_HOLD_ALLOWED_TIMER
|
||||
held_long = self._counter > BRAKE_HOLD_LIGHT_TIMER
|
||||
self.active = (firm_ready or held_long) and not self._released
|
||||
else:
|
||||
self._counter = 0
|
||||
self.active = False
|
||||
self._released = False
|
||||
self._armed = False
|
||||
|
||||
self._prev_brake_pressed = CS.out.brakePressed
|
||||
|
||||
can_sends = []
|
||||
if relay_blocked and frame % 2 == 0:
|
||||
override = self.active and not pcs_is_active(CS.pre_collision_2)
|
||||
can_sends.append(toyotacan.create_brake_hold_command(packer, frame, CS.pre_collision_2, override))
|
||||
|
||||
return can_sends
|
||||
@@ -0,0 +1,416 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import math
|
||||
import numpy as np
|
||||
|
||||
ONSET_T_BP = [0.0, 0.15, 0.6] # s
|
||||
# Speed schedule of the brake onset. Below 10 km/h (creep follow: the lead moves a little, the car catches up and
|
||||
# stops again) the Altis PCM bites hard on a modest request (route 000000ed 04:05:41: output -0.42 -> -1.25 m/s^2,
|
||||
# 1400 N; five ACC re-stops on 10-03/04 all -0.9..-1.25), so at creep speeds the onset uses the same numbers as the
|
||||
# engage schedule (driver 2026-10-04 "yes, apply it"): 0.5 m/s^3 for 0.2 s, stock by 0.7 s; from 20 km/h the normal
|
||||
# 1.0 m/s^3 for 0.1 s, stock by 0.4 s (driver 2026-10-03), interpolated between.
|
||||
# Driver 2026-10-04 after route 000000f2: "the brake after creeping forward again improved but is still not soft
|
||||
# enough, softer again" -> below 10 km/h 0.3 m/s^3 for 0.3 s (-0.09 m/s^2 at 0.3 s), stock by 0.9 s.
|
||||
# Driver 2026-10-05: "at 50 km/h the brake still feels heavy and there was plenty of distance; every brake should start
|
||||
# extremely light, and if that needs distance, start earlier" -> the creep-speed numbers (0.3 m/s^3 for 0.3 s, stock
|
||||
# by 0.9 s) now apply at every speed up to 60 km/h, fading to the highway onset (1.0 m/s^3, 0.1 s, stock by 0.4 s) at
|
||||
# 80 km/h, which stays as it was. The planner's light-onset margin (long_mpc) supplies the extra distance.
|
||||
# Driver 2026-10-05, same drive: "at 40-50 km/h the build from light to firm is still not smooth, a bit abrupt" -> the
|
||||
# jerk the onset ramps UP TO is also scheduled: 2.0 m/s^3 up to 60 km/h (was the stock 4.0 from 0.9 s on), stock at 80.
|
||||
ONSET_V_BP = [16.7, 22.2] # m/s (60, 80 km/h)
|
||||
ONSET_T1_V = [0.3, 0.1]
|
||||
ONSET_T3_V = [0.9, 0.4]
|
||||
ONSET_J1_V = [0.3, 1.0] # m/s^3 during the first phase
|
||||
ONSET_J3_V = [2.0, 4.0] # m/s^3 the ramp ends at (the shaper never exceeds the stock limit)
|
||||
ONSET_J_DOWN = [1.0, 1.0, 4.0] # m/s^3 (reference shape; the first two entries follow ONSET_J1_V)
|
||||
# Engaging ACC (SET-) while creeping up to a lead (driver 2026-10-04: "the car used to brake hard the moment I press
|
||||
# SET-; the first ~0.2 s must be a light touch, then blend into the decel the speed needs"). Route 000000ee 11:48:07:
|
||||
# engaged at 9 km/h 4 m behind a 4 km/h lead, the request went 0 -> -0.42 in 0.2 s and -1.6 by 0.8 s, the car hit
|
||||
# -2.7 m/s^2 (3280 N). While the engage window is open the down jerk is capped on time SINCE ENGAGING: 0.5 m/s^3 for
|
||||
# the first 0.2 s (-0.1 m/s^2 at 0.2 s), then rising to stock by 0.7 s; the planner's full request still arrives, just
|
||||
# later. The normal onset schedule applies on top (the stricter of the two wins).
|
||||
ENGAGE_BRAKE_T_BP = [0.0, 0.2, 0.7] # s since engaging
|
||||
ENGAGE_BRAKE_J_DOWN = [0.5, 0.5, 4.0] # m/s^3
|
||||
# Owner 2026-10-06: only a plan beyond -3.0 (a genuine emergency) bypasses the soft onset; -2.0 .. -3.0 keep it (at
|
||||
# 40 km/h -2.5 is reached slowly in ~1.8 s). History: -1.5 until 2026-10-03, -2.0, then -3.5 (= never) on 2026-10-05.
|
||||
HARD_BRAKE_ACCEL = -3.0
|
||||
URGENT_T = 0.1 # s
|
||||
URGENT_J = 1.0 # m/s^3
|
||||
URGENT_T_RAMP = 0.1 # s
|
||||
|
||||
|
||||
# Creep-follow gas release, with the creep-follow stop (see
|
||||
# stopping_controller.py). Route 0000010d 23:56:59: after a +0.5 launch the plan turned to braking within 0.2 s, but the
|
||||
# soft onset above treated the drop from +0.5 as the start of a brake (0.3 m/s^3) and the command stayed positive for
|
||||
# ~0.7 s while the car kept accelerating into a 2 m gap; then the brake bit 0 -> 1640 N. Taking away gas is not
|
||||
# braking: below CREEP_GAS_V the positive part of the command is now taken away at the stock rate; the soft onset
|
||||
# starts from zero, where the braking starts.
|
||||
CREEP_GAS_V = 8.0 / 3.6 # m/s
|
||||
|
||||
|
||||
# Friction entry (hybrid). Owner 2026-10-09: "no heavy-press jolt at the start of a brake - this is what I most want
|
||||
# gone; even -2.6 is fine if it is reached smoothly". On this hybrid the first ~-0.6 m/s^2 of a brake is regen; past it
|
||||
# the PCM brings in the friction brake (BRAKE 0xA6 BRAKE_FORCE), and how hard that bites depends on how fast the
|
||||
# command is moving at that moment. 27 engaged onsets with a friction entry, routes 00000109-00000119 (onset_bite.py):
|
||||
# command rate at friction entry -1.91 / -1.05 / -0.44 m/s^3 -> delivered jerk -3.70 / -2.22 / -1.22 m/s^3,
|
||||
# friction force rate 3067 / 1067 / 533 N/s (r = 0.78; gap command-delivered at entry r = 0.80).
|
||||
# The onset schedule above opens to 2.0 m/s^3 by 0.9 s - right when the friction usually enters (median 0.9 s), e.g.
|
||||
# route 00000119 09:23:02 (46 km/h, lead braking to a stop): command -0.9 -> -1.66 smoothly, the car sat at the regen
|
||||
# limit (-0.55), then the friction came in 680 -> 1520 N and the car went -0.76 -> -1.98 m/s^2 in 0.5 s.
|
||||
# So while the command is more than ENTRY_GAP beyond what the car delivers and the friction force is still below
|
||||
# ENTRY_FORCE_N, the command moves at most ENTRY_J: the friction enters slowly and the car catches up. After the entry
|
||||
# (force established or ENTRY_T_MAX) the command closes the remaining gap at CATCHUP_J, then the normal limits apply.
|
||||
# Not for FCW / urgent (< HARD_BRAKE_ACCEL) requests, below ENTRY_V_MIN (creep follow has its own onset), or without
|
||||
# measurements (a_ego None).
|
||||
ENTRY_GAP = 0.25 # m/s^2 the command may lead the delivered decel before the entry limit applies
|
||||
ENTRY_FORCE_N = 600.0 # N: friction established
|
||||
ENTRY_J = 0.5 # m/s^3 while the friction enters
|
||||
ENTRY_T_MAX = 1.0 # s per brake onset
|
||||
CATCHUP_J = 1.5 # m/s^3 after the entry until the command reaches the request
|
||||
ENTRY_V_MIN = 10.0 / 3.6 # m/s
|
||||
|
||||
|
||||
class BrakeOnsetShaper:
|
||||
def __init__(self, dt: float, stock_down_jerk: float):
|
||||
self.dt = dt
|
||||
self.stock_down_jerk = stock_down_jerk
|
||||
self.t_onset = 0.0
|
||||
self.t_entry = 0.0 # time spent limited by the friction entry in this brake
|
||||
self.catchup = False # after an entry: close the gap gently
|
||||
self.friction_in = False
|
||||
|
||||
def reset(self) -> None:
|
||||
self.t_onset = 0.0
|
||||
self.t_entry = 0.0
|
||||
self.catchup = False
|
||||
self.friction_in = False
|
||||
|
||||
def friction_entry_step(self, accel_request: float, prev_accel: float, v_ego: float, a_ego: float | None,
|
||||
brake_force: float) -> float | None:
|
||||
"""largest down step (> 0, m/s^2 per step) while the friction brake enters, None = no limit"""
|
||||
if a_ego is None or v_ego < ENTRY_V_MIN or prev_accel > -0.1:
|
||||
self.t_entry, self.catchup, self.friction_in = 0.0, False, False
|
||||
return None
|
||||
if math.isfinite(brake_force) and brake_force >= ENTRY_FORCE_N:
|
||||
self.friction_in = True
|
||||
if accel_request >= prev_accel:
|
||||
self.catchup = False
|
||||
return None
|
||||
leading = prev_accel - a_ego < -ENTRY_GAP
|
||||
if leading and not self.friction_in and self.t_entry < ENTRY_T_MAX:
|
||||
self.t_entry += self.dt
|
||||
self.catchup = True
|
||||
return ENTRY_J * self.dt
|
||||
if self.catchup:
|
||||
return CATCHUP_J * self.dt
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def schedule_t(v_ego: float) -> list[float]:
|
||||
t1 = float(np.interp(v_ego, ONSET_V_BP, ONSET_T1_V))
|
||||
t3 = float(np.interp(v_ego, ONSET_V_BP, ONSET_T3_V))
|
||||
return [0.0, t1, t3]
|
||||
|
||||
@staticmethod
|
||||
def schedule_j(v_ego: float) -> list[float]:
|
||||
j1 = float(np.interp(v_ego, ONSET_V_BP, ONSET_J1_V))
|
||||
j3 = float(np.interp(v_ego, ONSET_V_BP, ONSET_J3_V))
|
||||
return [j1, j1, j3]
|
||||
|
||||
def down_step(self, accel_request: float, prev_accel: float, bypass: bool = False, v_ego: float = 30.0,
|
||||
urgent: bool = False, t_engaged: float | None = None, a_ego: float | None = None,
|
||||
brake_force: float = float('nan')) -> float:
|
||||
if bypass:
|
||||
self.t_onset = 0.0
|
||||
self.t_entry, self.catchup = 0.0, False
|
||||
return -self.stock_down_jerk * self.dt
|
||||
entry = None if urgent else self.friction_entry_step(accel_request, prev_accel, v_ego, a_ego, brake_force)
|
||||
step = self._down_step(accel_request, prev_accel, v_ego, urgent, t_engaged)
|
||||
return step if entry is None else -min(-step, entry)
|
||||
|
||||
def _down_step(self, accel_request: float, prev_accel: float, v_ego: float, urgent: bool,
|
||||
t_engaged: float | None) -> float:
|
||||
if prev_accel > 0.0 and v_ego < CREEP_GAS_V and accel_request < prev_accel:
|
||||
self.t_onset = 0.0
|
||||
return -min(self.stock_down_jerk * self.dt, prev_accel - max(accel_request, 0.0))
|
||||
|
||||
if urgent:
|
||||
t_bp, j_bp = [0.0, URGENT_T, URGENT_T + max(URGENT_T_RAMP, self.dt)], [URGENT_J, URGENT_J, self.stock_down_jerk]
|
||||
else:
|
||||
t_bp, j_bp = self.schedule_t(v_ego), self.schedule_j(v_ego)
|
||||
gentlest_step = -ONSET_J_DOWN[0] * self.dt
|
||||
onset = (accel_request - prev_accel) < gentlest_step - 1e-9
|
||||
if onset:
|
||||
j_down = float(np.interp(self.t_onset, t_bp, j_bp))
|
||||
self.t_onset = min(self.t_onset + self.dt, t_bp[-1])
|
||||
else:
|
||||
j_down = self.stock_down_jerk
|
||||
self.t_onset = max(self.t_onset - self.dt, 0.0)
|
||||
if t_engaged is not None and t_engaged < ENGAGE_BRAKE_T_BP[-1]:
|
||||
j_down = min(j_down, float(np.interp(t_engaged, ENGAGE_BRAKE_T_BP, ENGAGE_BRAKE_J_DOWN)))
|
||||
return -min(j_down, self.stock_down_jerk) * self.dt
|
||||
|
||||
def is_urgent(self, accel_request: float, fcw: bool) -> bool:
|
||||
return fcw or accel_request < HARD_BRAKE_ACCEL
|
||||
|
||||
ENGAGE_T_BP = [0.0, 0.1, 0.6] # s; the window is still used to keep the soft BRAKE onset for a hard request after engaging
|
||||
# Driver 2026-10-04: "if Kumar's base added a soft start again, remove it - I want an immediate response, not a lurch,
|
||||
# following the eco/normal/sport profile". The gas-side engage ramp (0.25 -> 0.6 -> 4.0 m/s^3 over 0.6 s) is gone:
|
||||
# the up jerk after engaging is the stock windup limit; the accel profile sets how much.
|
||||
ENGAGE_J_UP = [4.0, 4.0, 4.0] # m/s^3
|
||||
# Resume while moving. Owner 2026-10-08: "when resuming the
|
||||
# speed (RES), can the first 0.4 s accelerate more gently?". Routes 0000010e-00000112, eight RES engagements at 15-61
|
||||
# km/h: the car was coasting at -0.3..-0.5 m/s^2 (regen), the command started from 0 and reached +0.3..+0.5 within
|
||||
# 0.4-0.6 s, so in the first 0.4 s the regen vanished AND the gas came in (aEgo -0.43 -> +0.19 in 0.4 s at 23:09:07,
|
||||
# +0.12 -> +0.64 in 0.6 s at 17:43:01, ~1.5-2 m/s^3). Now, when engaging above RESUME_V_MIN, the command starts from
|
||||
# the coasting decel (aEgo, clipped to RESUME_START_MIN..0) instead of 0, and rises at RESUME_J_UP: 0.6 m/s^3 for the
|
||||
# first 0.4 s, opening to the stock limit by 0.8 s. Replays of the logged plans: the command meets the stock one by
|
||||
# 0.6-0.8 s, identical afterwards (the eco/normal/sport profile is untouched). Standstill launches are not affected.
|
||||
RESUME_V_MIN = 15.0 / 3.6 # m/s
|
||||
RESUME_T_BP = [0.0, 0.4, 0.8] # s since engaging
|
||||
RESUME_J_UP = [0.6, 0.6, 4.0] # m/s^3
|
||||
RESUME_START_MIN = -0.6 # m/s^2
|
||||
RESUME_EDGE_T = 0.5 # s: the engagement must follow the cruise switching on (not a gas-override release)
|
||||
|
||||
|
||||
class EngageOnsetShaper:
|
||||
def __init__(self, dt: float, stock_up_jerk: float):
|
||||
self.dt = dt
|
||||
self.stock_up_jerk = stock_up_jerk
|
||||
self.t_engaged: float | None = None
|
||||
self.resume = False
|
||||
self.cruise_on_t = 1e9 # s since the PCM cruise went from off to on (RES / SET)
|
||||
self.prev_cruise = False
|
||||
|
||||
def reset(self) -> None:
|
||||
self.t_engaged = None
|
||||
self.resume = False
|
||||
|
||||
def cruise_state(self, cruise_enabled: bool) -> None:
|
||||
"""called every frame: tracks RES / SET (cruise off -> on), so a gas-override release is not a resume"""
|
||||
self.cruise_on_t = 0.0 if (cruise_enabled and not self.prev_cruise) else self.cruise_on_t + self.dt
|
||||
self.prev_cruise = cruise_enabled
|
||||
|
||||
def start_accel(self, prev_accel: float, a_ego: float, v_ego: float) -> float:
|
||||
"""on the first engaged frame of a resume while moving, the command starts from the coasting decel"""
|
||||
if self.t_engaged is None and v_ego > RESUME_V_MIN and self.cruise_on_t < RESUME_EDGE_T:
|
||||
self.resume = True
|
||||
return float(np.clip(a_ego, RESUME_START_MIN, 0.0))
|
||||
return prev_accel
|
||||
|
||||
@property
|
||||
def in_engage_window(self) -> bool:
|
||||
return self.t_engaged is None or self.t_engaged < ENGAGE_T_BP[-1]
|
||||
|
||||
@property
|
||||
def t_since_engage(self) -> float | None:
|
||||
"""seconds since engaging while the engage brake schedule still applies, else None"""
|
||||
t = 0.0 if self.t_engaged is None else self.t_engaged
|
||||
return t if t < ENGAGE_BRAKE_T_BP[-1] else None
|
||||
|
||||
def up_step(self, active: bool) -> float:
|
||||
if not active:
|
||||
self.t_engaged = None
|
||||
return self.stock_up_jerk * self.dt
|
||||
if self.t_engaged is None:
|
||||
self.t_engaged = 0.0
|
||||
else:
|
||||
self.t_engaged += self.dt
|
||||
if self.resume:
|
||||
if self.t_engaged < RESUME_T_BP[-1]:
|
||||
return min(float(np.interp(self.t_engaged, RESUME_T_BP, RESUME_J_UP)), self.stock_up_jerk) * self.dt
|
||||
self.resume = False
|
||||
if self.t_engaged >= ENGAGE_T_BP[-1]:
|
||||
return self.stock_up_jerk * self.dt
|
||||
j_up = float(np.interp(self.t_engaged, ENGAGE_T_BP, ENGAGE_J_UP))
|
||||
return min(j_up, self.stock_up_jerk) * self.dt
|
||||
|
||||
# Hard-brake overshoot limit (owner 2026-10-06, route 00000100 19:41:42-45: the lead braked from 44 km/h at -3.3 m/s^2;
|
||||
# openpilot asked for -3.49 at most, but the PCM with the hybrid's regen delivered -4.3 m/s^2 for 0.8 s (BRAKE_FORCE
|
||||
# 4880 N), ~30% more than asked - the last, heaviest part of the "sudden brake" feel. The stock PID only lifted the
|
||||
# command ~0.3 above the request.) Once the request is a hard brake, the car's own deceleration is watched: when it
|
||||
# runs OVERSHOOT_DEADBAND past the request (predicted a moment ahead, as the PID does), the command is lifted by
|
||||
# OVERSHOOT_GAIN times the excess, at most OVERSHOOT_MAX, so the car settles near what openpilot asked for. The request
|
||||
# itself is never weakened: the limit only takes back braking the car delivered ON TOP of it (an MPC replay of 19:41
|
||||
# with the asked-for -3.5 ends 4.9 m behind the stopped lead). It never lifts the command above OVERSHOOT_CMD_CEIL.
|
||||
OVERSHOOT_ACTIVE_ACCEL = -2.5 # m/s^2: the limit works only while the request is harder than this
|
||||
OVERSHOOT_DEADBAND = 0.2 # m/s^2 of overshoot that is left alone
|
||||
OVERSHOOT_GAIN = 0.8
|
||||
OVERSHOOT_MAX = 1.0 # m/s^2 largest lift
|
||||
OVERSHOOT_RATE_UP = 3.0 # m/s^3 how fast the lift may grow
|
||||
OVERSHOOT_RATE_DOWN = 2.0 # m/s^3 how fast it is given back (also when the hard request ends)
|
||||
OVERSHOOT_CMD_CEIL = -1.5 # m/s^2: the lifted command always stays a firm brake
|
||||
# Moderate band - Corolla Altis Hybrid only (owner 2026-10-07, route 0000010e 17:38:41: "at 50 km/h the braking started
|
||||
# soft, but the speed dropped too fast, still a bit fierce"). A stopped queue appeared 74 m ahead after a lane change;
|
||||
# openpilot asked for at most -1.60 m/s^2, the car delivered -1.82..-1.95 (regen on top). Routes 00000100..0000010e:
|
||||
# above 20 km/h the Altis delivers 0.1-0.4 m/s^2 more than a -1.0..-2.0 request (e.g. 0000010d 15-25 km/h: asked
|
||||
# -1.68, got -2.05). So above OVERSHOOT_MOD_V the limit also works for requests harder than OVERSHOOT_MOD_ACCEL, with
|
||||
# a smaller deadband, and the lifted command never gets lighter than OVERSHOOT_MOD_KEEP of the command (the request
|
||||
# is never weakened by more than a quarter). The hard band above is unchanged for every car.
|
||||
OVERSHOOT_MOD_ACCEL = -1.0 # m/s^2
|
||||
OVERSHOOT_MOD_V = 12.0 / 3.6 # m/s (0000010d 23:54:58: asked -1.72 at 17-23 km/h, delivered -1.96..-2.19)
|
||||
OVERSHOOT_MOD_DEADBAND = 0.15 # m/s^2
|
||||
OVERSHOOT_MOD_KEEP = 0.75
|
||||
# The moderate band works as a slow trim, not a fast limiter: a replay of 17:38:36-38 with the hard band's logic
|
||||
# (predicted aEgo, 3.0 / 2.0 m/s^3) pulsed the command by 0.2-0.3 m/s^2 for 0.2 s at a time on the noisy aEgo (-1.2 ..
|
||||
# -1.8 at a steady request), and the stock PID was already lifting the command (-1.6 asked, ~-1.1 sent). So the band
|
||||
# uses the overshoot of the measured aEgo, low-passed over OVERSHOOT_MOD_TAU, and moves at OVERSHOOT_MOD_RATE.
|
||||
# Relaxed personality only (owner 2026-10-08). A process_replay of 14 Aggressive-mode segments (routes fe/100/101/104/
|
||||
# 107/109) found the band acting mostly in CLOSE following: 0000109 23:19:44, lead 16 m away and still braking, request
|
||||
# -1.99/-1.75 at 26-21 km/h, the car delivered -2.73/-2.27 and the band lifted the command by up to 0.35 - taking away a
|
||||
# margin the close gap needs (open loop, no planner reaction: +6 m travelled). Tonight's Relaxed drives (routes
|
||||
# 00000112-00000116) never triggered it. Owner: "keep D in Relaxed". The carcontroller passes
|
||||
# hud_control.leadDistanceBars == 3 (controlsd: personality + 1; relaxed = 2 -> 3 bars).
|
||||
# OFF since 2026-10-08 (owner: "so D never acted in Relaxed last night? then switch it off and try"): routes
|
||||
# 00000112-00000116 never triggered it, and in Aggressive close following it removes margin. Kept in the code; set True
|
||||
# to bring it back (Relaxed only).
|
||||
OVERSHOOT_MOD_ENABLED = False
|
||||
OVERSHOOT_MOD_TAU = 0.5 # s
|
||||
OVERSHOOT_MOD_RATE = 0.5 # m/s^3
|
||||
|
||||
|
||||
class BrakeOvershootLimiter:
|
||||
def __init__(self, dt: float, moderate: bool = False):
|
||||
self.dt = dt
|
||||
self.moderate = moderate # also the moderate band (OVERSHOOT_MOD_*)
|
||||
self.lift = 0.0
|
||||
self.in_moderate = False
|
||||
self.mod_excess = 0.0
|
||||
|
||||
def reset(self) -> None:
|
||||
self.lift = 0.0
|
||||
self.in_moderate = False
|
||||
self.mod_excess = 0.0
|
||||
|
||||
def update(self, accel_request: float, a_ego_future: float, active: bool = True, v_ego: float = 0.0,
|
||||
a_ego: float | None = None, moderate_allowed: bool = True) -> float:
|
||||
"""returns the lift (>= 0, m/s^2) to add to the command"""
|
||||
target = 0.0
|
||||
hard = accel_request < OVERSHOOT_ACTIVE_ACCEL
|
||||
self.in_moderate = (self.moderate and moderate_allowed and not hard and accel_request < OVERSHOOT_MOD_ACCEL and
|
||||
v_ego > OVERSHOOT_MOD_V)
|
||||
if self.in_moderate:
|
||||
measured = a_ego_future if a_ego is None else a_ego
|
||||
alpha = self.dt / (OVERSHOOT_MOD_TAU + self.dt)
|
||||
self.mod_excess += alpha * ((accel_request - measured) - self.mod_excess)
|
||||
else:
|
||||
self.mod_excess = 0.0
|
||||
if active and hard:
|
||||
excess = accel_request - a_ego_future - OVERSHOOT_DEADBAND # > 0: decelerating harder than asked
|
||||
target = float(np.clip(OVERSHOOT_GAIN * excess, 0.0, OVERSHOOT_MAX))
|
||||
elif active and self.in_moderate:
|
||||
target = float(np.clip(OVERSHOOT_GAIN * (self.mod_excess - OVERSHOOT_MOD_DEADBAND), 0.0, OVERSHOOT_MAX))
|
||||
if self.in_moderate and not hard:
|
||||
step = OVERSHOOT_MOD_RATE * self.dt
|
||||
self.lift = float(np.clip(target, self.lift - step, self.lift + step))
|
||||
else:
|
||||
self.lift = float(np.clip(target, self.lift - OVERSHOOT_RATE_DOWN * self.dt, self.lift + OVERSHOOT_RATE_UP * self.dt))
|
||||
return self.lift
|
||||
|
||||
def apply(self, accel_cmd: float) -> float:
|
||||
if self.lift <= 0.0:
|
||||
return accel_cmd
|
||||
ceiling = OVERSHOOT_MOD_KEEP * accel_cmd if self.in_moderate else OVERSHOOT_CMD_CEIL
|
||||
return min(accel_cmd + self.lift, max(accel_cmd, ceiling))
|
||||
|
||||
|
||||
# Low-speed regen hand-over feed-forward. History: route 0000010d 23:54:58-23:55:03 - between 20 and 9 km/h the car cut its own
|
||||
# friction brake (BRAKE 0xA6 BRAKE_FORCE 1640 -> 440 N) and delivered ~70% of the request, felt as "brakes, goes light,
|
||||
# brakes again". A feedback compensation (shortfall x 0.8 past a 0.2 deadband, 1.0 m/s^3) was the first fix; its
|
||||
# first drive, route 0000010e 2026-10-07, showed it acting only in 0.1-0.25 m/s^2 pulses that never filled the gap
|
||||
# (owner: "the stops are sometimes soft, sometimes still not smooth; several stops still braked twice").
|
||||
# Measured on routes 00000100..0000010e (engaged stops, request / delivered, m/s^2): the request eases smoothly, but the
|
||||
# delivered decel DIPS at 15-9 km/h (10e: -0.84 asked, -0.61 delivered; 109: -0.87 / -0.69; 107: -1.04 / -0.81) and
|
||||
# comes back ON TOP of the request at 7-1 km/h (10e: -0.47 / -0.60; 109: -0.54 / -0.80), the second part pushed by the
|
||||
# PID integrator that wound up during the dip (command - request -0.10..-0.17 there). So, instead of feedback:
|
||||
# - a FEED-FORWARD extra brake by speed, the measured dip: HANDOVER_EXTRA_MAX (or HANDOVER_EXTRA_FRAC of a lighter
|
||||
# request) at 10-14 km/h, fading to 0 at 6 and 17 km/h, changing at HANDOVER_RATE, only while braking harder than
|
||||
# HANDOVER_MIN_REQUEST;
|
||||
# - below PID_HOLD_V, while the request brakes, the PID integrator is frozen and bled toward zero (a positive
|
||||
# integral, left over from a launch, faster), so the catch-up no longer adds braking at the end of the stop.
|
||||
# Expected delivered decel on 10e (by speed band 15-12/12-9/9-7/7-5/5-3/3-1 km/h): -0.86/-0.81/-0.75/-0.75/-0.63/-0.48,
|
||||
# continuous easing, instead of -0.66/-0.61/-0.75/-0.75/-0.71/-0.60.
|
||||
HANDOVER_V_BP = [6.0 / 3.6, 8.0 / 3.6, 10.0 / 3.6, 14.0 / 3.6, 17.0 / 3.6] # m/s
|
||||
HANDOVER_V_W = [0.0, 0.5, 1.0, 1.0, 0.0]
|
||||
HANDOVER_MIN_REQUEST = -0.3 # m/s^2
|
||||
HANDOVER_EXTRA_MAX = 0.2 # m/s^2
|
||||
HANDOVER_EXTRA_FRAC = 0.25 # of the request, for lighter requests
|
||||
HANDOVER_RATE = 1.0 # m/s^3
|
||||
# The dip is a moderate-brake effect: at a firm request the regen gives MORE, not less (0000010d 15-12 km/h: asked
|
||||
# -1.46, delivered -1.72), so the extra fades out between these requests (full at -1.0 and lighter, none at -1.5).
|
||||
HANDOVER_REQ_BP = [-1.5, -1.0] # m/s^2
|
||||
HANDOVER_REQ_W = [0.0, 1.0]
|
||||
# Owner 2026-10-10 (route 0000011c 00:19:10, Aggressive stop 1.6 m behind the lead): "the end of the stop slides too
|
||||
# much, the last two metres feel too light". With the PID held below PID_HOLD_V nothing makes up the under-delivery
|
||||
# below the hand-over band; asked / delivered by band (m/s^2), three stops of 11c (00:18:07 / 00:19:10 / 00:20:58):
|
||||
# 8-7 km/h -0.75/-0.62 -0.89/-0.58 -0.77/-0.54 6-5 km/h -0.63/-0.45 -0.75/-0.50 -0.66/-0.49
|
||||
# 5-4 km/h -0.60/-0.40 -0.67/-0.28 -0.64/-0.35 4-3 km/h -0.58/-0.62 -0.62/-0.45 -0.61/-0.57
|
||||
# 3-2 km/h on target or above. Brake force 650-850 N at 6-4 km/h, below what beats the hybrid creep torque.
|
||||
# So a second, low-speed feed-forward: LOW_EXTRA at LOW_EXTRA_V_BP (0.25 at 5-6.5 km/h, gone by 3 km/h, led by about
|
||||
# 0.5 km/h for the ~0.3 s actuator lag), added to the hand-over extra, same request gate and rate.
|
||||
LOW_EXTRA_V_BP = [3.0 / 3.6, 4.0 / 3.6, 5.0 / 3.6, 6.5 / 3.6, 8.5 / 3.6] # m/s
|
||||
LOW_EXTRA = [0.0, 0.15, 0.25, 0.25, 0.0] # m/s^2
|
||||
PID_HOLD_V = 9.0 / 3.6 # m/s
|
||||
PID_BLEED_NEG = 0.5 # m/s^2 per s: a braking integral fades this fast
|
||||
PID_BLEED_POS = 2.0 # m/s^2 per s: a gas integral (left from the launch) fades this fast
|
||||
|
||||
class BrakeHandoverFeedforward:
|
||||
def __init__(self, dt: float):
|
||||
self.dt = dt
|
||||
self.extra = 0.0
|
||||
|
||||
def reset(self) -> None:
|
||||
self.extra = 0.0
|
||||
|
||||
def update(self, accel_request: float, v_ego: float, active: bool = True) -> float:
|
||||
"""returns the extra braking (>= 0, m/s^2) to subtract from the command"""
|
||||
target = 0.0
|
||||
if active and accel_request < HANDOVER_MIN_REQUEST:
|
||||
w = float(np.interp(v_ego, HANDOVER_V_BP, HANDOVER_V_W)) * float(np.interp(accel_request, HANDOVER_REQ_BP, HANDOVER_REQ_W))
|
||||
target = w * min(HANDOVER_EXTRA_MAX, HANDOVER_EXTRA_FRAC * -accel_request)
|
||||
target += float(np.interp(v_ego, LOW_EXTRA_V_BP, LOW_EXTRA)) * float(np.interp(accel_request, HANDOVER_REQ_BP, HANDOVER_REQ_W))
|
||||
step = HANDOVER_RATE * self.dt
|
||||
self.extra = float(np.clip(target, self.extra - step, self.extra + step))
|
||||
return self.extra
|
||||
|
||||
def apply(self, accel_cmd: float) -> float:
|
||||
return accel_cmd - self.extra
|
||||
|
||||
def condition_pid(self, pid, accel_request: float, v_ego: float) -> bool:
|
||||
"""below PID_HOLD_V while braking: bleed the integrator toward zero; returns True = freeze it this step"""
|
||||
if v_ego >= PID_HOLD_V or accel_request >= 0.0:
|
||||
return False
|
||||
if pid.i > 0.0:
|
||||
pid.i = max(0.0, pid.i - PID_BLEED_POS * self.dt)
|
||||
else:
|
||||
pid.i = min(0.0, pid.i + PID_BLEED_NEG * self.dt)
|
||||
return True
|
||||
|
||||
|
||||
class BrakeCommandCorrections:
|
||||
"""tnpb2 corrections to the final braking command."""
|
||||
def __init__(self, dt: float):
|
||||
self.overshoot = BrakeOvershootLimiter(dt, moderate=OVERSHOOT_MOD_ENABLED)
|
||||
self.handover = BrakeHandoverFeedforward(dt)
|
||||
|
||||
def reset(self) -> None:
|
||||
self.overshoot.reset()
|
||||
self.handover.reset()
|
||||
|
||||
def apply(self, accel_cmd: float, accel_request: float, a_ego_future: float, v_ego: float, stopping: bool, fcw: bool,
|
||||
a_ego: float | None = None, relaxed: bool = True) -> float:
|
||||
# take back braking the car delivers beyond a hard request (never under FCW); the moderate band only in Relaxed
|
||||
self.overshoot.update(accel_request, a_ego_future, active=not stopping and not fcw, v_ego=v_ego, a_ego=a_ego,
|
||||
moderate_allowed=relaxed)
|
||||
accel_cmd = self.overshoot.apply(accel_cmd)
|
||||
# feed-forward the braking the car does not deliver in the low-speed regen hand-over
|
||||
self.handover.update(accel_request, v_ego, active=not fcw)
|
||||
return self.handover.apply(accel_cmd)
|
||||
|
||||
def condition_pid(self, pid, accel_request: float, v_ego: float) -> bool:
|
||||
"""called before the stock PID update; True = freeze the integrator this step"""
|
||||
return self.handover.condition_pid(pid, accel_request, v_ego)
|
||||
@@ -11,8 +11,11 @@ from opendbc.car import Bus, structs
|
||||
from opendbc.car.carlog import carlog
|
||||
from opendbc.can.parser import CANParser
|
||||
from opendbc.car.common.conversions import Conversions as CV
|
||||
from opendbc.car.toyota.values import ToyotaFlags
|
||||
from opendbc.sunnypilot.car.toyota.values import ToyotaFlagsSP
|
||||
|
||||
ENGINE_RUNNING_RPM = 300.0
|
||||
|
||||
TRAFFIC_SIGNAL_MAP = {
|
||||
1: "kph",
|
||||
36: "mph",
|
||||
@@ -168,3 +171,7 @@ class CarStateExt:
|
||||
# Update traffic signals and speed limit
|
||||
self.update_traffic_signals(cp_cam)
|
||||
ret_sp.speedLimit = self.calculate_speed_limit()
|
||||
|
||||
if self.CP.flags & ToyotaFlags.HYBRID and "ENGINE_RPM" in cp.vl:
|
||||
ret_sp.engineRpm = float(cp.vl["ENGINE_RPM"]["RPM"])
|
||||
ret_sp.engineOff = ret_sp.engineRpm < ENGINE_RUNNING_RPM and not bool(cp.vl["ENGINE_RPM"]["ENGINE_RUNNING"])
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.can_definitions import CanData
|
||||
from opendbc.car.toyota import toyotacan
|
||||
from opendbc.sunnypilot.car.toyota.values import ToyotaFlagsSP
|
||||
|
||||
LEFT_BLINDSPOT = b"\x41"
|
||||
RIGHT_BLINDSPOT = b"\x42"
|
||||
LEFT_SIDE = LEFT_BLINDSPOT[0]
|
||||
RIGHT_SIDE = RIGHT_BLINDSPOT[0]
|
||||
|
||||
# BLINDSPOTD1/D2 aren't real distances despite the DBC name: verified against 3 real routes, values
|
||||
# fall into 3 clean bands - idle (0), a small transitional-noise band (seen 1-31, rare: single-digit
|
||||
# occurrence counts, present during side/state transitions), and two real zone codes (~46-47 and
|
||||
# ~50-53, consistent across routes and cars - likely mirroring stock BSM's own ADJACENT/APPROACHING
|
||||
# split). A plain nonzero check lets the noise band through as false "occupied" hits; gate on the
|
||||
# real-zone floor instead - comfortably above every noise value seen, comfortably below neither zone code.
|
||||
BLINDSPOT_NOISE_FLOOR = 35
|
||||
|
||||
# DEBUG also carries a 1-bit BLINDSPOT flag (byte 4) that isn't read here. Checked against the same
|
||||
# route: stayed 0 across all frames, including every confirmed real detection - not a usable signal.
|
||||
|
||||
|
||||
class EnhancedBsm:
|
||||
def __init__(self, CP: structs.CarParams, CP_SP: structs.CarParamsSP):
|
||||
self.CP = CP
|
||||
self.CP_SP = CP_SP
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
return bool(self.CP_SP.flags & ToyotaFlagsSP.SP_ENHANCED_BSM)
|
||||
|
||||
|
||||
class _BsmSideState:
|
||||
def __init__(self):
|
||||
self.blindspot = False
|
||||
self.counter = 0
|
||||
|
||||
def update(self, distance_1, distance_2):
|
||||
# every fresh matching-side reading directly reflects the current occupancy check - this is not
|
||||
# a latch. A drop to an idle/noise reading on this exact side clears it immediately, same as the
|
||||
# original. The counter is purely a silence timeout for when this side stops responding entirely,
|
||||
# not a "hold the last detection for a while" mechanism.
|
||||
self.blindspot = distance_1 > BLINDSPOT_NOISE_FLOOR or distance_2 > BLINDSPOT_NOISE_FLOOR
|
||||
self.counter = 100
|
||||
|
||||
def decay(self):
|
||||
self.counter = max(0, self.counter - 1)
|
||||
if self.counter == 0:
|
||||
self.blindspot = False
|
||||
|
||||
|
||||
# Enhanced BSM (@arne182, @rav4kumar)
|
||||
class EnhancedBsmCarState(EnhancedBsm):
|
||||
def __init__(self, CP: structs.CarParams, CP_SP: structs.CarParamsSP):
|
||||
super().__init__(CP, CP_SP)
|
||||
|
||||
self._sides = {LEFT_SIDE: _BsmSideState(), RIGHT_SIDE: _BsmSideState()}
|
||||
|
||||
def update(self, cp, frame: int) -> tuple[bool, bool]:
|
||||
# Let's keep all the commented out code for easy debug purposes in the future.
|
||||
distance_1 = cp.vl["DEBUG"].get('BLINDSPOTD1')
|
||||
distance_2 = cp.vl["DEBUG"].get('BLINDSPOTD2')
|
||||
side = cp.vl["DEBUG"].get('BLINDSPOTSIDE')
|
||||
|
||||
if all(val is not None for val in [distance_1, distance_2, side]) and side in self._sides:
|
||||
self._sides[side].update(distance_1, distance_2)
|
||||
|
||||
for side_state in self._sides.values():
|
||||
side_state.decay()
|
||||
|
||||
return self._sides[LEFT_SIDE].blindspot, self._sides[RIGHT_SIDE].blindspot
|
||||
|
||||
|
||||
class _BsmSideController:
|
||||
def __init__(self, addr_byte: bytes):
|
||||
self.addr_byte = addr_byte
|
||||
self.debug_enabled = False
|
||||
self.last_poll_frame = 0
|
||||
|
||||
def update(self, frame: int, poll_phase: int, e_bsm_rate: int, always_on: bool, vego_ok: bool) -> list[CanData]:
|
||||
can_sends = []
|
||||
|
||||
if not self.debug_enabled:
|
||||
if always_on or vego_ok: # eagle eye camera will stop working if bsm is switched on under 6m/s
|
||||
can_sends.append(toyotacan.create_set_bsm_debug_mode(self.addr_byte, True))
|
||||
self.debug_enabled = True
|
||||
self.last_poll_frame = frame # give the poll loop a fresh baseline so the stale-poll disable check below can't fire before the first real poll
|
||||
else:
|
||||
# no periodic re-assert: re-sending DiagnosticSessionControl(extendedSession) while already in that
|
||||
# session appears to make the ECU intermittently drop its own detection state (confirmed against a
|
||||
# real route - two reasserts landed inside an 8s window where the ECU went silent on that side).
|
||||
# send it once and leave it alone, matching the original, proven-stable behavior.
|
||||
if not always_on and frame - self.last_poll_frame > 50:
|
||||
can_sends.append(toyotacan.create_set_bsm_debug_mode(self.addr_byte, False))
|
||||
self.debug_enabled = False
|
||||
|
||||
if frame % e_bsm_rate == poll_phase:
|
||||
can_sends.append(toyotacan.create_bsm_polling_status(self.addr_byte))
|
||||
self.last_poll_frame = frame
|
||||
|
||||
return can_sends
|
||||
|
||||
|
||||
# Enhanced BSM (@arne182, @rav4kumar)
|
||||
class EnhancedBsmCarController(EnhancedBsm):
|
||||
def __init__(self, CP: structs.CarParams, CP_SP: structs.CarParamsSP):
|
||||
super().__init__(CP, CP_SP)
|
||||
|
||||
self._left = _BsmSideController(LEFT_BLINDSPOT)
|
||||
self._right = _BsmSideController(RIGHT_BLINDSPOT)
|
||||
|
||||
def update(self, CS: structs.CarState, frame: int, e_bsm_rate: int = 20, always_on: bool = True) -> list[CanData]:
|
||||
if frame <= 200:
|
||||
return []
|
||||
|
||||
vego_ok = CS.out.vEgo > 6
|
||||
can_sends = self._left.update(frame, 0, e_bsm_rate, always_on, vego_ok)
|
||||
can_sends += self._right.update(frame, e_bsm_rate // 2, e_bsm_rate, always_on, vego_ok)
|
||||
return can_sends
|
||||
@@ -0,0 +1,231 @@
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from opendbc.car import structs
|
||||
from opendbc.sunnypilot.car.toyota.auto_brake_hold import AutoBrakeHoldCarController, BRAKE_HOLD_ALLOWED_TIMER, pcs_is_active
|
||||
from opendbc.sunnypilot.car.toyota.values import ToyotaFlagsSP
|
||||
|
||||
GearShifter = structs.CarState.GearShifter
|
||||
|
||||
|
||||
def make_car_params_sp(enabled: bool = True) -> structs.CarParamsSP:
|
||||
cp_sp = structs.CarParamsSP()
|
||||
cp_sp.flags = ToyotaFlagsSP.SP_AUTO_BRAKE_HOLD if enabled else 0
|
||||
return cp_sp
|
||||
|
||||
|
||||
class FakeCarState:
|
||||
def __init__(self, standstill=True, cruise_enabled=False, cruise_available=True, gas_pressed=False,
|
||||
gear=GearShifter.drive, brake_pressed=False, pre_collision_2=None):
|
||||
self.out = structs.CarState()
|
||||
self.out.standstill = standstill
|
||||
self.out.cruiseState.enabled = cruise_enabled
|
||||
self.out.cruiseState.available = cruise_available
|
||||
self.out.gasPressed = gas_pressed
|
||||
self.out.gearShifter = gear
|
||||
self.out.brakePressed = brake_pressed
|
||||
self.pre_collision_2 = pre_collision_2 if pre_collision_2 is not None else {}
|
||||
|
||||
|
||||
@patch("opendbc.sunnypilot.car.toyota.auto_brake_hold.toyotacan.create_brake_hold_command")
|
||||
class TestAutoBrakeHoldCarController(unittest.TestCase):
|
||||
def _make(self):
|
||||
return AutoBrakeHoldCarController(structs.CarParams(), make_car_params_sp())
|
||||
|
||||
def test_enabled_reflects_flag(self, mock_create):
|
||||
for value in range(256):
|
||||
with self.subTest(flags=value):
|
||||
cp_sp = structs.CarParamsSP()
|
||||
cp_sp.flags = value
|
||||
ctrl = AutoBrakeHoldCarController(structs.CarParams(), cp_sp)
|
||||
self.assertEqual(ctrl.enabled, bool(value & ToyotaFlagsSP.SP_AUTO_BRAKE_HOLD))
|
||||
|
||||
def test_does_not_engage_before_timer(self, mock_create):
|
||||
ctrl = self._make()
|
||||
cs = FakeCarState(brake_pressed=False)
|
||||
for i in range(BRAKE_HOLD_ALLOWED_TIMER):
|
||||
ctrl.update(cs, i, None)
|
||||
self.assertFalse(ctrl.active)
|
||||
|
||||
def test_engages_after_timer_once_brake_released(self, mock_create):
|
||||
ctrl = self._make()
|
||||
cs = FakeCarState(brake_pressed=False)
|
||||
for i in range(BRAKE_HOLD_ALLOWED_TIMER + 1):
|
||||
ctrl.update(cs, i, None)
|
||||
self.assertTrue(ctrl.active)
|
||||
|
||||
def test_brake_still_down_from_the_stop_does_not_block_engagement(self, mock_create):
|
||||
# the driver's foot is normally still on the brake on the very frame standstill is reached -
|
||||
# that must not count as a "fresh press" release, or the feature could never engage
|
||||
ctrl = self._make()
|
||||
# decelerating into the stop with the brake held continuously
|
||||
cs = FakeCarState(standstill=False, brake_pressed=True)
|
||||
for i in range(30):
|
||||
ctrl.update(cs, i, None)
|
||||
# reaches standstill, foot stays down for a while, then lifts
|
||||
cs.out.standstill = True
|
||||
for i in range(30, 50):
|
||||
ctrl.update(cs, i, None)
|
||||
self.assertFalse(ctrl.active, "must not engage while the original stopping press is still held")
|
||||
cs.out.brakePressed = False
|
||||
for i in range(50, 50 + BRAKE_HOLD_ALLOWED_TIMER + 1):
|
||||
ctrl.update(cs, i, None)
|
||||
self.assertTrue(ctrl.active, "should engage once the stopping press is released and the timer elapses")
|
||||
|
||||
def test_fresh_brake_press_mid_hold_releases_and_stays_released(self, mock_create):
|
||||
ctrl = self._make()
|
||||
cs = FakeCarState(brake_pressed=False)
|
||||
frame = 0
|
||||
for _ in range(BRAKE_HOLD_ALLOWED_TIMER + 1):
|
||||
ctrl.update(cs, frame, None)
|
||||
frame += 1
|
||||
self.assertTrue(ctrl.active)
|
||||
|
||||
cs.out.brakePressed = True
|
||||
ctrl.update(cs, frame, None)
|
||||
frame += 1
|
||||
self.assertFalse(ctrl.active, "a fresh press should release immediately")
|
||||
|
||||
for _ in range(20):
|
||||
ctrl.update(cs, frame, None)
|
||||
frame += 1
|
||||
self.assertFalse(ctrl.active, "must stay released for the rest of the episode while continuously held")
|
||||
|
||||
cs.out.brakePressed = False
|
||||
for _ in range(20):
|
||||
ctrl.update(cs, frame, None)
|
||||
frame += 1
|
||||
self.assertFalse(ctrl.active, "must stay released for the rest of the episode even after lifting off again")
|
||||
|
||||
def test_drive_off_and_restop_rearms(self, mock_create):
|
||||
ctrl = self._make()
|
||||
cs = FakeCarState(brake_pressed=False)
|
||||
frame = 0
|
||||
for _ in range(BRAKE_HOLD_ALLOWED_TIMER + 1):
|
||||
ctrl.update(cs, frame, None)
|
||||
frame += 1
|
||||
cs.out.brakePressed = True
|
||||
ctrl.update(cs, frame, None)
|
||||
frame += 1
|
||||
self.assertFalse(ctrl.active)
|
||||
|
||||
# drives off - leaves the standstill episode entirely
|
||||
cs.out.standstill = False
|
||||
ctrl.update(cs, frame, None)
|
||||
frame += 1
|
||||
|
||||
# stops again
|
||||
cs.out.standstill = True
|
||||
cs.out.brakePressed = False
|
||||
for _ in range(BRAKE_HOLD_ALLOWED_TIMER + 1):
|
||||
ctrl.update(cs, frame, None)
|
||||
frame += 1
|
||||
self.assertTrue(ctrl.active, "should re-arm and engage again at the next stop")
|
||||
|
||||
def test_cruise_engaged_blocks_hold(self, mock_create):
|
||||
ctrl = self._make()
|
||||
cs = FakeCarState(cruise_enabled=True, brake_pressed=False)
|
||||
for i in range(BRAKE_HOLD_ALLOWED_TIMER + 1):
|
||||
ctrl.update(cs, i, None)
|
||||
self.assertFalse(ctrl.active, "must not hold while ACC is engaged - that's the point of the constraint")
|
||||
|
||||
def test_gas_pressed_blocks_hold(self, mock_create):
|
||||
ctrl = self._make()
|
||||
cs = FakeCarState(gas_pressed=True, brake_pressed=False)
|
||||
for i in range(BRAKE_HOLD_ALLOWED_TIMER + 1):
|
||||
ctrl.update(cs, i, None)
|
||||
self.assertFalse(ctrl.active)
|
||||
|
||||
def test_park_and_reverse_block_hold(self, mock_create):
|
||||
for gear in (GearShifter.park, GearShifter.reverse):
|
||||
with self.subTest(gear=gear):
|
||||
ctrl = self._make()
|
||||
cs = FakeCarState(gear=gear, brake_pressed=False)
|
||||
for i in range(BRAKE_HOLD_ALLOWED_TIMER + 1):
|
||||
ctrl.update(cs, i, None)
|
||||
self.assertFalse(ctrl.active)
|
||||
|
||||
def test_yields_to_live_pcs_without_dropping_active_state(self, mock_create):
|
||||
ctrl = self._make()
|
||||
cs = FakeCarState(brake_pressed=False)
|
||||
frame = 0
|
||||
for _ in range(BRAKE_HOLD_ALLOWED_TIMER + 1):
|
||||
ctrl.update(cs, frame, None)
|
||||
frame += 1
|
||||
self.assertTrue(ctrl.active)
|
||||
mock_create.reset_mock()
|
||||
|
||||
# a real PCS event shows up on the live signal
|
||||
cs.pre_collision_2 = {"PCSALM": 1}
|
||||
# advance to the next even frame the message is actually built on
|
||||
while frame % 2 != 0:
|
||||
frame += 1
|
||||
ctrl.update(cs, frame, None)
|
||||
override_arg = mock_create.call_args.args[-1]
|
||||
self.assertTrue(ctrl.active, "internal hold state should not be cleared by a live PCS event")
|
||||
self.assertFalse(override_arg, "must not override PRE_COLLISION_2 while PCS is genuinely active")
|
||||
|
||||
# once PCS goes quiet again, override resumes on our own signal, not stale PCS state
|
||||
frame += 2
|
||||
cs.pre_collision_2 = {}
|
||||
ctrl.update(cs, frame, None)
|
||||
override_arg = mock_create.call_args.args[-1]
|
||||
self.assertTrue(override_arg)
|
||||
|
||||
def test_message_only_built_every_other_frame(self, mock_create):
|
||||
ctrl = self._make()
|
||||
cs = FakeCarState(brake_pressed=False)
|
||||
for i in range(10):
|
||||
ctrl.update(cs, i, None)
|
||||
self.assertEqual(mock_create.call_count, 5)
|
||||
|
||||
def test_no_message_sent_outside_relay_blocked_window(self, mock_create):
|
||||
# outside relay_blocked, toyota_fwd_hook lets the real PRE_COLLISION_2 relay through on its own -
|
||||
# our passthrough copy would just be redundant traffic panda rejects, so it must not be sent at all
|
||||
cases = [
|
||||
dict(cruise_enabled=True),
|
||||
dict(gas_pressed=True),
|
||||
dict(cruise_available=False),
|
||||
]
|
||||
for kwargs in cases:
|
||||
with self.subTest(kwargs=kwargs):
|
||||
ctrl = self._make()
|
||||
cs = FakeCarState(brake_pressed=False, **kwargs)
|
||||
for i in range(10):
|
||||
ctrl.update(cs, i, None)
|
||||
mock_create.assert_not_called()
|
||||
|
||||
def test_message_still_sent_in_park_or_reverse(self, mock_create):
|
||||
# relay_blocked has no gear check, matching toyota_fwd_hook - park/reverse must not open a gap
|
||||
# in the PRE_COLLISION_2 relay even though hold can never engage there (this was a real
|
||||
# regression: gating the send on hold_allowed's gear check left bus 0 with nothing at this
|
||||
# address while parked, which is what tripped a genuine PCS dash fault on-road)
|
||||
for gear in (GearShifter.park, GearShifter.reverse):
|
||||
with self.subTest(gear=gear):
|
||||
ctrl = self._make()
|
||||
cs = FakeCarState(gear=gear, brake_pressed=False)
|
||||
for i in range(BRAKE_HOLD_ALLOWED_TIMER + 1):
|
||||
ctrl.update(cs, i, None)
|
||||
self.assertFalse(ctrl.active, "must never actually hold in park/reverse")
|
||||
self.assertGreater(mock_create.call_count, 0, "must still relay PRE_COLLISION_2 in park/reverse")
|
||||
override_arg = mock_create.call_args.args[-1]
|
||||
self.assertFalse(override_arg, "never override while gear disallows an actual hold")
|
||||
|
||||
|
||||
class TestPcsIsActive(unittest.TestCase):
|
||||
def test_all_zero_is_not_active(self):
|
||||
self.assertFalse(pcs_is_active({}))
|
||||
self.assertFalse(pcs_is_active({"PCSALM": 0, "DSS1GDRV": 0}))
|
||||
|
||||
def test_any_trigger_field_is_active(self):
|
||||
for field in ("PCSALM", "IBTRGR", "PBATRGR", "PREFILL", "AVSTRGR", "PBRTRGR", "PPTRGR"):
|
||||
with self.subTest(field=field):
|
||||
self.assertTrue(pcs_is_active({field: 1}))
|
||||
|
||||
def test_nonzero_force_signal_is_active(self):
|
||||
self.assertTrue(pcs_is_active({"DSS1GDRV": -5}))
|
||||
self.assertFalse(pcs_is_active({"DSS1GDRV": 0}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,171 @@
|
||||
import numpy as np
|
||||
|
||||
from opendbc.car import rate_limit, DT_CTRL
|
||||
from opendbc.sunnypilot.car.toyota.brake_onset import BrakeOnsetShaper, ONSET_J_DOWN, HARD_BRAKE_ACCEL
|
||||
|
||||
DT = DT_CTRL * 3
|
||||
STOCK_J = 4.0
|
||||
UP_STEP = 4.0 * DT
|
||||
|
||||
|
||||
def run(requests, a0=0.0, fcw=False):
|
||||
shaper = BrakeOnsetShaper(DT, STOCK_J)
|
||||
a = a0
|
||||
out = []
|
||||
for req in requests:
|
||||
step = shaper.down_step(req, a, bypass=shaper.is_urgent(req, fcw))
|
||||
a = rate_limit(req, a, step, UP_STEP)
|
||||
out.append(a)
|
||||
return np.array(out)
|
||||
|
||||
|
||||
def at(out, t):
|
||||
return out[int(round(t / DT)) - 1]
|
||||
|
||||
|
||||
class TestBrakeOnset:
|
||||
def test_step_brake_eases_in_over_the_first_second(self):
|
||||
out = run([-1.5] * 60)
|
||||
assert at(out, 0.09) > -0.06 # first 0.15 s: barely anything
|
||||
assert at(out, 0.21) > -0.12 # 0.2 s: still very light
|
||||
assert -0.45 < at(out, 0.36) < -0.10 # ~0.35 s: building slowly
|
||||
assert -0.60 < at(out, 0.60) < -0.15 # 0.6 s: still gentle, about a fifth of the way
|
||||
assert -1.10 < at(out, 1.00) < -0.55 # 1.0 s: about half way, the ramp is now running up
|
||||
assert np.isclose(at(out, 1.5), -1.5, atol=1e-6) # settled once the ramp reaches the stock limit
|
||||
|
||||
def test_jerk_follows_the_schedule_and_never_exceeds_stock(self):
|
||||
out = run([-1.9] * 60, a0=1.0) # large step, still above the hard-brake bypass
|
||||
jerks = np.diff(np.concatenate([[1.0], out])) / DT
|
||||
assert jerks.min() >= -STOCK_J - 1e-6
|
||||
assert jerks[0] >= -ONSET_J_DOWN[0] - 1e-6
|
||||
assert jerks[int(0.09 / DT)] >= -ONSET_J_DOWN[1] - 1e-6
|
||||
# the blend is one straight ramp: jerk keeps growing monotonically until the stock limit
|
||||
ramp = -jerks[int(0.1 / DT):int(0.6 / DT)]
|
||||
assert np.all(np.diff(ramp) >= -1e-6)
|
||||
|
||||
def test_lead_switch_during_a_settled_brake_gets_the_same_soft_onset(self):
|
||||
reqs = [-0.6] * 70 + [-1.8] * 60 # long enough for the schedule to wind fully back before the switch
|
||||
out = run(reqs)
|
||||
t_switch = 70 * DT
|
||||
assert np.isclose(at(out, t_switch), -0.6, atol=1e-6)
|
||||
assert at(out, t_switch + 0.09) > -0.6 - 0.05
|
||||
assert at(out, t_switch + 0.21) > -0.6 - 0.25
|
||||
assert np.isclose(out[-1], -1.8, atol=1e-6)
|
||||
|
||||
def test_hard_brake_request_bypasses_the_schedule(self):
|
||||
out = run([HARD_BRAKE_ACCEL - 0.5] * 20)
|
||||
assert np.isclose(out[0], -STOCK_J * DT, atol=1e-6)
|
||||
assert at(out, 0.3) <= -1.1
|
||||
|
||||
def test_fcw_bypasses_the_schedule(self):
|
||||
out = run([-1.5] * 20, fcw=True)
|
||||
assert np.isclose(out[0], -STOCK_J * DT, atol=1e-6)
|
||||
|
||||
def test_release_is_untouched(self):
|
||||
out = run([1.0] * 20, a0=-1.5)
|
||||
assert np.isclose(out[0], -1.5 + UP_STEP, atol=1e-6)
|
||||
|
||||
def test_gentle_request_passes_straight_through(self):
|
||||
reqs = list(np.linspace(0.0, -0.2, 40)) # ~0.17 m/s^3, below the gentlest onset limit (0.25)
|
||||
out = run(reqs)
|
||||
assert np.allclose(out, reqs, atol=1e-9)
|
||||
|
||||
def test_short_pause_does_not_fully_rearm(self):
|
||||
shaper = BrakeOnsetShaper(DT, STOCK_J)
|
||||
a = 0.0
|
||||
for _ in range(int(0.3 / DT)):
|
||||
a = rate_limit(-1.5, a, shaper.down_step(-1.5, a), UP_STEP)
|
||||
t_after_ramp = shaper.t_onset
|
||||
for _ in range(2): # hold for two frames, request steady
|
||||
shaper.down_step(a, a)
|
||||
assert 0.0 < shaper.t_onset < t_after_ramp
|
||||
for _ in range(int(1.0 / DT)): # a full second of settled brake winds it back completely
|
||||
shaper.down_step(a, a)
|
||||
assert shaper.t_onset == 0.0
|
||||
|
||||
|
||||
class TestEngageBrake:
|
||||
"""2026-10-04: SET- while creeping up to a lead: the first 0.2 s after engaging is a light touch."""
|
||||
def test_first_0_2_s_after_engaging_is_light_then_blends_to_stock(self):
|
||||
from opendbc.sunnypilot.car.toyota.brake_onset import BrakeOnsetShaper, EngageOnsetShaper, ENGAGE_BRAKE_T_BP
|
||||
dt = 0.03
|
||||
shaper, engage = BrakeOnsetShaper(dt, 4.0), EngageOnsetShaper(dt, 4.0)
|
||||
a, t, trace = 0.0, 0.0, {}
|
||||
while t < 1.6:
|
||||
engage.up_step(True)
|
||||
step = shaper.down_step(-1.65, a, v_ego=2.5, t_engaged=engage.t_since_engage)
|
||||
a = max(-1.65, a + step)
|
||||
t += dt
|
||||
for mark in (0.2, 0.5, 1.5):
|
||||
if mark not in trace and t >= mark:
|
||||
trace[mark] = a
|
||||
assert trace[0.2] > -0.12 # light first touch (at creep speed the creep onset schedule is the stricter one)
|
||||
assert -1.0 < trace[0.5] < -0.15 # blending in
|
||||
assert trace[1.5] < -1.4 # the planner's request has arrived (end jerk 2.0 m/s^3 below 60 km/h)
|
||||
assert ENGAGE_BRAKE_T_BP[-1] <= 1.0
|
||||
|
||||
|
||||
class TestCreepOnset:
|
||||
"""2026-10-04: a brake onset at creep speed (re-stop behind a lead) is as light as the engage schedule."""
|
||||
def test_below_10kph_first_0_2_s_is_light(self):
|
||||
from opendbc.sunnypilot.car.toyota.brake_onset import BrakeOnsetShaper
|
||||
dt = 0.03
|
||||
for v, light_limit in ((1.0, -0.15), (30.0, -0.45)): # creep: barely anything by 0.2 s; highway: the normal onset
|
||||
shaper = BrakeOnsetShaper(dt, 4.0)
|
||||
a, t, at_02 = 0.0, 0.0, None
|
||||
while t < 1.2:
|
||||
a = max(-1.0, a + shaper.down_step(-1.0, a, v_ego=v))
|
||||
t += dt
|
||||
if at_02 is None and t >= 0.2:
|
||||
at_02 = a
|
||||
assert at_02 > light_limit, (v, at_02)
|
||||
assert a < -0.9, (v, a) # the full request arrives within the window at both speeds
|
||||
|
||||
|
||||
class TestCreepGasRelease:
|
||||
"""route 0000010d 23:56:59: the drop from a +0.5 launch request was shaped like a brake onset"""
|
||||
def _shaper(self):
|
||||
from opendbc.sunnypilot.car.toyota.brake_onset import BrakeOnsetShaper
|
||||
return BrakeOnsetShaper(DT_CTRL * 3, 4.0)
|
||||
|
||||
def test_positive_part_goes_at_the_stock_rate_below_8kph(self):
|
||||
sh = self._shaper()
|
||||
step = sh.down_step(-0.24, 0.5, v_ego=2.0 / 3.6)
|
||||
assert abs(step - (-4.0 * DT_CTRL * 3)) < 1e-9
|
||||
step = sh.down_step(-0.24, 0.05, v_ego=2.0 / 3.6)
|
||||
assert abs(step - (-0.05)) < 1e-9 # stops at zero; the braking starts from there
|
||||
|
||||
def test_faster_keeps_the_soft_onset(self):
|
||||
step = self._shaper().down_step(-0.24, 0.5, v_ego=20.0 / 3.6)
|
||||
assert step > -4.0 * DT_CTRL * 3 + 1e-9
|
||||
|
||||
|
||||
class TestResumeSoftStart:
|
||||
"""RES while coasting at 46 km/h: start from the coasting decel, 0.6 m/s^3 for 0.4 s, stock by 0.8 s"""
|
||||
def _run(self, a_ego=-0.45, v=46.0, req=0.4, edge=True, n=40):
|
||||
from opendbc.sunnypilot.car.toyota.brake_onset import EngageOnsetShaper
|
||||
sh = EngageOnsetShaper(DT, 4.0)
|
||||
sh.cruise_state(False)
|
||||
sh.cruise_state(True if edge else False)
|
||||
if not edge:
|
||||
for _ in range(30):
|
||||
sh.cruise_state(True)
|
||||
prev, out = 0.0, []
|
||||
for _ in range(n):
|
||||
prev = sh.start_accel(prev, a_ego, v / 3.6)
|
||||
prev = rate_limit(req, prev, -4.0 * DT, sh.up_step(True))
|
||||
out.append(prev)
|
||||
return np.array(out)
|
||||
|
||||
def test_gentle_first_0_4s_then_stock(self):
|
||||
out = self._run()
|
||||
assert abs(out[0] - (-0.45 + 0.6 * DT)) < 1e-9 # starts from the coasting decel
|
||||
assert at(out, 0.39) < -0.45 + 0.6 * 0.4 + 0.02 # 0.6 m/s^3 for the first 0.4 s
|
||||
assert np.isclose(out[-1], 0.4) # reaches the plan
|
||||
|
||||
def test_not_at_low_speed_or_a_gas_override_release(self):
|
||||
assert self._run(v=10.0)[0] == 4.0 * DT
|
||||
assert self._run(edge=False)[0] == 4.0 * DT
|
||||
|
||||
def test_a_positive_a_ego_starts_from_zero(self):
|
||||
assert abs(self._run(a_ego=0.3)[0] - 0.6 * DT) < 1e-9
|
||||
@@ -0,0 +1,105 @@
|
||||
import unittest
|
||||
|
||||
from opendbc.sunnypilot.car.toyota.brake_onset import (BrakeOvershootLimiter, OVERSHOOT_CMD_CEIL, OVERSHOOT_DEADBAND,
|
||||
OVERSHOOT_MAX, OVERSHOOT_MOD_DEADBAND, OVERSHOOT_MOD_KEEP,
|
||||
OVERSHOOT_MOD_RATE)
|
||||
|
||||
DT = 0.03
|
||||
|
||||
|
||||
class TestBrakeOvershootLimiter(unittest.TestCase):
|
||||
def run_steady(self, req, a_ego, n=200, active=True):
|
||||
lim = BrakeOvershootLimiter(DT)
|
||||
for _ in range(n):
|
||||
lim.update(req, a_ego, active)
|
||||
return lim
|
||||
|
||||
def test_moderate_braking_is_untouched(self):
|
||||
self.assertEqual(self.run_steady(-2.0, -3.0).lift, 0.0)
|
||||
|
||||
def test_overshoot_inside_the_deadband_is_untouched(self):
|
||||
self.assertEqual(self.run_steady(-3.4, -3.4 - OVERSHOOT_DEADBAND + 0.01).lift, 0.0)
|
||||
|
||||
def test_19_41_overshoot_is_taken_back(self):
|
||||
lim = self.run_steady(-3.4, -4.3)
|
||||
self.assertAlmostEqual(lim.lift, 0.8 * (0.9 - OVERSHOOT_DEADBAND), places=6)
|
||||
self.assertAlmostEqual(lim.apply(-3.4), -3.4 + lim.lift, places=6)
|
||||
|
||||
def test_under_braking_never_lifts(self):
|
||||
self.assertEqual(self.run_steady(-3.4, -2.5).lift, 0.0)
|
||||
|
||||
def test_lift_is_capped_and_command_stays_a_firm_brake(self):
|
||||
lim = self.run_steady(-2.6, -6.0)
|
||||
self.assertEqual(lim.lift, OVERSHOOT_MAX)
|
||||
self.assertAlmostEqual(lim.apply(-2.6), -2.6 + OVERSHOOT_MAX)
|
||||
self.assertEqual(lim.apply(-2.2), OVERSHOOT_CMD_CEIL) # a PID-lightened command is not lifted past a firm brake
|
||||
|
||||
def test_lift_grows_and_fades_gradually(self):
|
||||
lim = BrakeOvershootLimiter(DT)
|
||||
prev = 0.0
|
||||
for _ in range(10):
|
||||
lift = lim.update(-3.4, -5.0)
|
||||
self.assertLessEqual(lift - prev, 3.0 * DT + 1e-9)
|
||||
prev = lift
|
||||
for _ in range(10):
|
||||
lift = lim.update(-1.0, -1.0) # the hard request ends
|
||||
self.assertLessEqual(prev - lift, 2.0 * DT + 1e-9)
|
||||
prev = lift
|
||||
|
||||
def test_inactive_gives_it_back(self):
|
||||
lim = self.run_steady(-3.4, -4.3)
|
||||
for _ in range(100):
|
||||
lim.update(-3.4, -4.3, active=False)
|
||||
self.assertEqual(lim.lift, 0.0)
|
||||
|
||||
|
||||
class TestModerateBand(unittest.TestCase):
|
||||
"""Altis Hybrid only: route 0000010e 17:38:41, asked -1.60 at 40-50 km/h, the car delivered -1.95."""
|
||||
def run_steady(self, req, a_ego, v_kph, moderate=True, n=200):
|
||||
lim = BrakeOvershootLimiter(DT, moderate=moderate)
|
||||
for _ in range(n):
|
||||
lim.update(req, a_ego, True, v_ego=v_kph / 3.6)
|
||||
return lim
|
||||
|
||||
def test_17_38_41_overshoot_is_taken_back(self):
|
||||
lim = self.run_steady(-1.60, -1.95, 45.0)
|
||||
self.assertAlmostEqual(lim.lift, 0.8 * (0.35 - OVERSHOOT_MOD_DEADBAND), places=4) # low-pass converged
|
||||
self.assertAlmostEqual(lim.apply(-1.60), -1.60 + lim.lift, places=6)
|
||||
|
||||
def test_never_lighter_than_three_quarters_of_the_command(self):
|
||||
lim = self.run_steady(-1.2, -3.0, 45.0)
|
||||
self.assertAlmostEqual(lim.apply(-1.2), OVERSHOOT_MOD_KEEP * -1.2, places=6)
|
||||
|
||||
def test_off_for_other_cars_low_speed_and_light_requests(self):
|
||||
self.assertEqual(self.run_steady(-1.60, -1.95, 45.0, moderate=False).lift, 0.0)
|
||||
self.assertEqual(self.run_steady(-1.60, -1.95, 10.0).lift, 0.0)
|
||||
self.assertEqual(self.run_steady(-0.9, -1.4, 45.0).lift, 0.0)
|
||||
self.assertEqual(self.run_steady(-1.60, -1.70, 45.0).lift, 0.0) # inside the deadband
|
||||
|
||||
def test_hard_band_unchanged_with_moderate_on(self):
|
||||
lim = self.run_steady(-3.4, -4.3, 45.0)
|
||||
self.assertAlmostEqual(lim.lift, 0.8 * (0.9 - OVERSHOOT_DEADBAND), places=6)
|
||||
self.assertAlmostEqual(lim.apply(-3.4), -3.4 + lim.lift, places=6)
|
||||
|
||||
def test_moderate_band_is_a_slow_smooth_trim(self):
|
||||
lim = BrakeOvershootLimiter(DT, moderate=True)
|
||||
prev = 0.0
|
||||
for k in range(100):
|
||||
a = -1.95 if k % 2 else -1.45 # noisy aEgo around -1.70 at a steady -1.60 request
|
||||
lift = lim.update(-1.60, a, True, v_ego=45 / 3.6, a_ego=a)
|
||||
self.assertLessEqual(abs(lift - prev), OVERSHOOT_MOD_RATE * DT + 1e-9)
|
||||
prev = lift
|
||||
self.assertEqual(lift, 0.0) # averaged overshoot 0.10 is inside the deadband
|
||||
|
||||
def test_moderate_band_only_in_relaxed(self):
|
||||
lim = BrakeOvershootLimiter(DT, moderate=True)
|
||||
for _ in range(200):
|
||||
lim.update(-1.60, -1.95, True, v_ego=45 / 3.6, moderate_allowed=False)
|
||||
self.assertEqual(lim.lift, 0.0)
|
||||
for _ in range(200):
|
||||
lim.update(-3.4, -4.3, True, v_ego=45 / 3.6, moderate_allowed=False) # the hard band in every personality
|
||||
self.assertGreater(lim.lift, 0.5)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,66 @@
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
|
||||
from opendbc.sunnypilot.car.toyota.brake_onset import (BrakeHandoverFeedforward, HANDOVER_EXTRA_MAX,
|
||||
HANDOVER_EXTRA_FRAC, HANDOVER_RATE, PID_BLEED_NEG, PID_BLEED_POS)
|
||||
|
||||
DT = 0.03
|
||||
class TestHandoverFeedforward(unittest.TestCase):
|
||||
def run_steady(self, req, v_kph, n=200):
|
||||
c = BrakeHandoverFeedforward(DT)
|
||||
for _ in range(n):
|
||||
c.update(req, v_kph / 3.6)
|
||||
return c
|
||||
|
||||
def test_full_extra_in_the_dip(self):
|
||||
c = self.run_steady(-0.84, 12.0) # route 0000010e: asked -0.84 at 12-9 km/h, the car gave -0.61
|
||||
self.assertAlmostEqual(c.extra, HANDOVER_EXTRA_MAX, places=9)
|
||||
self.assertAlmostEqual(c.apply(-0.84), -0.84 - HANDOVER_EXTRA_MAX, places=9)
|
||||
|
||||
def test_lighter_request_gets_a_quarter(self):
|
||||
self.assertAlmostEqual(self.run_steady(-0.5, 12.0).extra, HANDOVER_EXTRA_FRAC * 0.5, places=9)
|
||||
|
||||
def test_fades_at_the_edges(self):
|
||||
self.assertAlmostEqual(self.run_steady(-1.0, 8.0).extra, 0.5 * HANDOVER_EXTRA_MAX, places=6)
|
||||
self.assertEqual(self.run_steady(-1.0, 5.0).extra, 0.0)
|
||||
self.assertEqual(self.run_steady(-1.0, 20.0).extra, 0.0)
|
||||
self.assertEqual(self.run_steady(-0.25, 12.0).extra, 0.0)
|
||||
|
||||
def test_fades_out_for_a_firm_request(self):
|
||||
self.assertEqual(self.run_steady(-1.5, 12.0).extra, 0.0) # 0000010d: at -1.46 the car delivered -1.72
|
||||
self.assertAlmostEqual(self.run_steady(-1.25, 12.0).extra, 0.5 * HANDOVER_EXTRA_MAX, places=6)
|
||||
|
||||
def test_gradual(self):
|
||||
c = BrakeHandoverFeedforward(DT)
|
||||
prev = 0.0
|
||||
for _ in range(50):
|
||||
e = c.update(-1.0, 12 / 3.6)
|
||||
self.assertLessEqual(e - prev, HANDOVER_RATE * DT + 1e-9)
|
||||
prev = e
|
||||
|
||||
class TestPidHold(unittest.TestCase):
|
||||
def test_braking_integral_bleeds_and_freezes_below_9kph(self):
|
||||
c = BrakeHandoverFeedforward(DT)
|
||||
pid = SimpleNamespace(i=-0.15)
|
||||
self.assertTrue(c.condition_pid(pid, -0.6, 5 / 3.6))
|
||||
self.assertAlmostEqual(pid.i, -0.15 + PID_BLEED_NEG * DT, places=9)
|
||||
for _ in range(100):
|
||||
c.condition_pid(pid, -0.6, 5 / 3.6)
|
||||
self.assertEqual(pid.i, 0.0)
|
||||
|
||||
def test_gas_integral_left_from_a_launch_bleeds_fast(self):
|
||||
c = BrakeHandoverFeedforward(DT)
|
||||
pid = SimpleNamespace(i=0.3)
|
||||
c.condition_pid(pid, -0.2, 3 / 3.6)
|
||||
self.assertAlmostEqual(pid.i, 0.3 - PID_BLEED_POS * DT, places=9)
|
||||
|
||||
def test_untouched_above_9kph_or_when_not_braking(self):
|
||||
c = BrakeHandoverFeedforward(DT)
|
||||
pid = SimpleNamespace(i=-0.15)
|
||||
self.assertFalse(c.condition_pid(pid, -0.6, 12 / 3.6))
|
||||
self.assertFalse(c.condition_pid(pid, 0.3, 3 / 3.6))
|
||||
self.assertEqual(pid.i, -0.15)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,15 @@
|
||||
import unittest
|
||||
|
||||
from opendbc.car.toyota.carstate import AccelPersonality, get_accel_personality
|
||||
|
||||
|
||||
class TestDriveMode(unittest.TestCase):
|
||||
def test_acceleration_profile_mapping(self):
|
||||
self.assertEqual(get_accel_personality(0, 0), AccelPersonality.normal)
|
||||
self.assertEqual(get_accel_personality(0, 1), AccelPersonality.eco)
|
||||
self.assertEqual(get_accel_personality(1, 0), AccelPersonality.sport)
|
||||
self.assertEqual(get_accel_personality(1, 1), AccelPersonality.sport)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,57 @@
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.toyota.carcontroller import get_long_tune
|
||||
from opendbc.car.toyota.values import CAR, ToyotaFlags
|
||||
from opendbc.sunnypilot.car.interfaces import _initialize_toyota
|
||||
from opendbc.sunnypilot.car.toyota.values import ToyotaFlagsSP
|
||||
|
||||
|
||||
def build_params(*, tss2=True):
|
||||
flags = ToyotaFlags.TSS2.value if tss2 else 0
|
||||
CP = structs.CarParams(
|
||||
brand="toyota",
|
||||
carFingerprint=str(CAR.TOYOTA_COROLLA_TSS2),
|
||||
flags=flags,
|
||||
)
|
||||
return CP, structs.CarParamsSP()
|
||||
|
||||
|
||||
class TestTss2LongTuning(unittest.TestCase):
|
||||
def test_param_handoff(self):
|
||||
for enabled in (False, True):
|
||||
with self.subTest(enabled=enabled):
|
||||
CP, CP_SP = build_params()
|
||||
_initialize_toyota(CP, CP_SP, {"ToyotaTSS2Long": int(enabled)})
|
||||
self.assertEqual(bool(CP_SP.flags & ToyotaFlagsSP.TSS2_LONG_TUNING), enabled)
|
||||
|
||||
def test_tss2_tune_selection(self):
|
||||
controller_params = SimpleNamespace(ACCEL_MAX=2.0, ACCEL_MIN=-3.5)
|
||||
CP, CP_SP = build_params()
|
||||
|
||||
stock_tune = get_long_tune(CP, CP_SP, controller_params)
|
||||
stock_tune.speed = 2.0
|
||||
self.assertEqual(stock_tune.k_i, 0.5)
|
||||
stock_tune.speed = 5.0
|
||||
self.assertEqual(stock_tune.k_i, 0.25)
|
||||
|
||||
CP_SP.flags |= ToyotaFlagsSP.TSS2_LONG_TUNING.value
|
||||
custom_tune = get_long_tune(CP, CP_SP, controller_params)
|
||||
custom_tune.speed = 0.0
|
||||
self.assertEqual(custom_tune.k_i, 0.30)
|
||||
custom_tune.speed = 5.0
|
||||
self.assertEqual(custom_tune.k_i, 0.28)
|
||||
|
||||
def test_non_tss2_ignores_custom_tune_flag(self):
|
||||
controller_params = SimpleNamespace(ACCEL_MAX=2.0, ACCEL_MIN=-3.5)
|
||||
CP, CP_SP = build_params(tss2=False)
|
||||
CP_SP.flags |= ToyotaFlagsSP.TSS2_LONG_TUNING.value
|
||||
|
||||
standard_tune = get_long_tune(CP, CP_SP, controller_params)
|
||||
standard_tune.speed = 0.0
|
||||
self.assertEqual(standard_tune.k_i, 3.6)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -14,6 +14,11 @@ class ToyotaFlagsSP(IntFlag):
|
||||
ZSS = 4
|
||||
STOCK_LONGITUDINAL = 8
|
||||
STOP_AND_GO_HACK = 16
|
||||
SP_ENHANCED_BSM = 32
|
||||
SP_NEED_DEBUG_BSM = 64
|
||||
SP_AUTO_BRAKE_HOLD = 128
|
||||
SP_BRAKE_ONSET = 256
|
||||
TSS2_LONG_TUNING = 512
|
||||
|
||||
|
||||
class ToyotaSafetyFlagsSP:
|
||||
|
||||
@@ -204,11 +204,16 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
|
||||
aTarget @5 :Float32;
|
||||
events @6 :List(OnroadEventSP.Event);
|
||||
e2eAlerts @7 :E2eAlerts;
|
||||
accelController @8 :AccelController;
|
||||
|
||||
struct DynamicExperimentalControl {
|
||||
state @0 :DynamicExperimentalControlState;
|
||||
enabled @1 :Bool;
|
||||
active @2 :Bool;
|
||||
decelIntent @3 :Float32;
|
||||
curveDetected @4 :Bool;
|
||||
wantBlended @5 :Bool;
|
||||
leadVeto @6 :Bool;
|
||||
|
||||
enum DynamicExperimentalControlState {
|
||||
acc @0;
|
||||
@@ -306,6 +311,17 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
|
||||
greenLightAlert @0 :Bool;
|
||||
leadDepartAlert @1 :Bool;
|
||||
}
|
||||
|
||||
struct AccelController {
|
||||
enabled @0 :Bool;
|
||||
active @1 :Bool;
|
||||
profile @2 :Profile;
|
||||
enum Profile {
|
||||
eco @0;
|
||||
normal @1;
|
||||
sport @2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct OnroadEventSP @0xda96579883444c35 {
|
||||
@@ -447,6 +463,8 @@ struct BackupManagerSP @0xf98d843bfd7004a3 {
|
||||
|
||||
struct CarStateSP @0xb86e6369214c01c8 {
|
||||
speedLimit @0 :Float32;
|
||||
engineOff @1 :Bool;
|
||||
engineRpm @2 :Float32;
|
||||
}
|
||||
|
||||
struct LiveMapDataSP @0xf416ec09499d9d19 {
|
||||
|
||||
@@ -1897,34 +1897,36 @@ const ::capnp::_::RawSchema s_e60821c0505ad473 = {
|
||||
4, 12, i_e60821c0505ad473, nullptr, nullptr, { &s_e60821c0505ad473, nullptr, nullptr, 0, 0, nullptr }, false
|
||||
};
|
||||
#endif // !CAPNP_LITE
|
||||
static const ::capnp::_::AlignedData<172> b_f35cc4560bbf6ec2 = {
|
||||
static const ::capnp::_::AlignedData<192> b_f35cc4560bbf6ec2 = {
|
||||
{ 0, 0, 0, 0, 5, 0, 6, 0,
|
||||
194, 110, 191, 11, 86, 196, 92, 243,
|
||||
13, 0, 0, 0, 1, 0, 2, 0,
|
||||
89, 10, 85, 29, 102, 186, 38, 181,
|
||||
5, 0, 7, 0, 0, 0, 0, 0,
|
||||
6, 0, 7, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
21, 0, 0, 0, 2, 1, 0, 0,
|
||||
33, 0, 0, 0, 87, 0, 0, 0,
|
||||
33, 0, 0, 0, 103, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
125, 0, 0, 0, 199, 1, 0, 0,
|
||||
141, 0, 0, 0, 255, 1, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
99, 117, 115, 116, 111, 109, 46, 99,
|
||||
97, 112, 110, 112, 58, 76, 111, 110,
|
||||
103, 105, 116, 117, 100, 105, 110, 97,
|
||||
108, 80, 108, 97, 110, 83, 80, 0,
|
||||
20, 0, 0, 0, 1, 0, 1, 0,
|
||||
24, 0, 0, 0, 1, 0, 1, 0,
|
||||
119, 191, 14, 16, 174, 91, 106, 188,
|
||||
33, 0, 0, 0, 218, 0, 0, 0,
|
||||
41, 0, 0, 0, 218, 0, 0, 0,
|
||||
163, 23, 92, 92, 71, 153, 141, 193,
|
||||
41, 0, 0, 0, 154, 0, 0, 0,
|
||||
49, 0, 0, 0, 154, 0, 0, 0,
|
||||
187, 34, 168, 255, 81, 184, 158, 154,
|
||||
45, 0, 0, 0, 90, 0, 0, 0,
|
||||
53, 0, 0, 0, 90, 0, 0, 0,
|
||||
196, 110, 217, 86, 5, 68, 71, 173,
|
||||
45, 0, 0, 0, 186, 0, 0, 0,
|
||||
53, 0, 0, 0, 186, 0, 0, 0,
|
||||
254, 40, 174, 34, 232, 188, 103, 165,
|
||||
49, 0, 0, 0, 82, 0, 0, 0,
|
||||
57, 0, 0, 0, 82, 0, 0, 0,
|
||||
161, 196, 243, 6, 208, 214, 220, 136,
|
||||
57, 0, 0, 0, 130, 0, 0, 0,
|
||||
68, 121, 110, 97, 109, 105, 99, 69,
|
||||
120, 112, 101, 114, 105, 109, 101, 110,
|
||||
116, 97, 108, 67, 111, 110, 116, 114,
|
||||
@@ -1939,63 +1941,72 @@ static const ::capnp::_::AlignedData<172> b_f35cc4560bbf6ec2 = {
|
||||
83, 111, 117, 114, 99, 101, 0, 0,
|
||||
69, 50, 101, 65, 108, 101, 114, 116,
|
||||
115, 0, 0, 0, 0, 0, 0, 0,
|
||||
32, 0, 0, 0, 3, 0, 4, 0,
|
||||
65, 99, 99, 101, 108, 67, 111, 110,
|
||||
116, 114, 111, 108, 108, 101, 114, 0,
|
||||
36, 0, 0, 0, 3, 0, 4, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 1, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
209, 0, 0, 0, 34, 0, 0, 0,
|
||||
237, 0, 0, 0, 34, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
204, 0, 0, 0, 3, 0, 1, 0,
|
||||
216, 0, 0, 0, 2, 0, 1, 0,
|
||||
232, 0, 0, 0, 3, 0, 1, 0,
|
||||
244, 0, 0, 0, 2, 0, 1, 0,
|
||||
1, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 1, 0, 1, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
213, 0, 0, 0, 186, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
216, 0, 0, 0, 3, 0, 1, 0,
|
||||
228, 0, 0, 0, 2, 0, 1, 0,
|
||||
2, 0, 0, 0, 1, 0, 0, 0,
|
||||
0, 0, 1, 0, 2, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
225, 0, 0, 0, 154, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
228, 0, 0, 0, 3, 0, 1, 0,
|
||||
240, 0, 0, 0, 2, 0, 1, 0,
|
||||
3, 0, 0, 0, 2, 0, 0, 0,
|
||||
0, 0, 1, 0, 3, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
237, 0, 0, 0, 90, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
236, 0, 0, 0, 3, 0, 1, 0,
|
||||
248, 0, 0, 0, 2, 0, 1, 0,
|
||||
4, 0, 0, 0, 1, 0, 0, 0,
|
||||
0, 0, 1, 0, 4, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
245, 0, 0, 0, 66, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
240, 0, 0, 0, 3, 0, 1, 0,
|
||||
252, 0, 0, 0, 2, 0, 1, 0,
|
||||
5, 0, 0, 0, 2, 0, 0, 0,
|
||||
0, 0, 1, 0, 5, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
249, 0, 0, 0, 66, 0, 0, 0,
|
||||
241, 0, 0, 0, 186, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
244, 0, 0, 0, 3, 0, 1, 0,
|
||||
0, 1, 0, 0, 2, 0, 1, 0,
|
||||
6, 0, 0, 0, 3, 0, 0, 0,
|
||||
0, 0, 1, 0, 6, 0, 0, 0,
|
||||
2, 0, 0, 0, 1, 0, 0, 0,
|
||||
0, 0, 1, 0, 2, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
253, 0, 0, 0, 58, 0, 0, 0,
|
||||
253, 0, 0, 0, 154, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
248, 0, 0, 0, 3, 0, 1, 0,
|
||||
0, 1, 0, 0, 3, 0, 1, 0,
|
||||
12, 1, 0, 0, 2, 0, 1, 0,
|
||||
3, 0, 0, 0, 2, 0, 0, 0,
|
||||
0, 0, 1, 0, 3, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
9, 1, 0, 0, 90, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
8, 1, 0, 0, 3, 0, 1, 0,
|
||||
20, 1, 0, 0, 2, 0, 1, 0,
|
||||
7, 0, 0, 0, 4, 0, 0, 0,
|
||||
0, 0, 1, 0, 7, 0, 0, 0,
|
||||
4, 0, 0, 0, 1, 0, 0, 0,
|
||||
0, 0, 1, 0, 4, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
17, 1, 0, 0, 82, 0, 0, 0,
|
||||
17, 1, 0, 0, 66, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
12, 1, 0, 0, 3, 0, 1, 0,
|
||||
24, 1, 0, 0, 2, 0, 1, 0,
|
||||
5, 0, 0, 0, 2, 0, 0, 0,
|
||||
0, 0, 1, 0, 5, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
21, 1, 0, 0, 66, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
16, 1, 0, 0, 3, 0, 1, 0,
|
||||
28, 1, 0, 0, 2, 0, 1, 0,
|
||||
6, 0, 0, 0, 3, 0, 0, 0,
|
||||
0, 0, 1, 0, 6, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
25, 1, 0, 0, 58, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
20, 1, 0, 0, 3, 0, 1, 0,
|
||||
48, 1, 0, 0, 2, 0, 1, 0,
|
||||
7, 0, 0, 0, 4, 0, 0, 0,
|
||||
0, 0, 1, 0, 7, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
45, 1, 0, 0, 82, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
44, 1, 0, 0, 3, 0, 1, 0,
|
||||
56, 1, 0, 0, 2, 0, 1, 0,
|
||||
8, 0, 0, 0, 5, 0, 0, 0,
|
||||
0, 0, 1, 0, 8, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
53, 1, 0, 0, 130, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
52, 1, 0, 0, 3, 0, 1, 0,
|
||||
64, 1, 0, 0, 2, 0, 1, 0,
|
||||
100, 101, 99, 0, 0, 0, 0, 0,
|
||||
16, 0, 0, 0, 0, 0, 0, 0,
|
||||
119, 191, 14, 16, 174, 91, 106, 188,
|
||||
@@ -2067,6 +2078,15 @@ static const ::capnp::_::AlignedData<172> b_f35cc4560bbf6ec2 = {
|
||||
254, 40, 174, 34, 232, 188, 103, 165,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
16, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
97, 99, 99, 101, 108, 67, 111, 110,
|
||||
116, 114, 111, 108, 108, 101, 114, 0,
|
||||
16, 0, 0, 0, 0, 0, 0, 0,
|
||||
161, 196, 243, 6, 208, 214, 220, 136,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
16, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, }
|
||||
@@ -2074,6 +2094,7 @@ static const ::capnp::_::AlignedData<172> b_f35cc4560bbf6ec2 = {
|
||||
::capnp::word const* const bp_f35cc4560bbf6ec2 = b_f35cc4560bbf6ec2.words;
|
||||
#if !CAPNP_LITE
|
||||
static const ::capnp::_::RawSchema* const d_f35cc4560bbf6ec2[] = {
|
||||
&s_88dcd6d006f3c4a1,
|
||||
&s_9a9eb851ffa822bb,
|
||||
&s_a567bce822ae28fe,
|
||||
&s_ad47440556d96ec4,
|
||||
@@ -2081,14 +2102,14 @@ static const ::capnp::_::RawSchema* const d_f35cc4560bbf6ec2[] = {
|
||||
&s_c18d99475c5c17a3,
|
||||
&s_f6e831752fcdf793,
|
||||
};
|
||||
static const uint16_t m_f35cc4560bbf6ec2[] = {5, 0, 7, 6, 1, 2, 3, 4};
|
||||
static const uint16_t i_f35cc4560bbf6ec2[] = {0, 1, 2, 3, 4, 5, 6, 7};
|
||||
static const uint16_t m_f35cc4560bbf6ec2[] = {5, 8, 0, 7, 6, 1, 2, 3, 4};
|
||||
static const uint16_t i_f35cc4560bbf6ec2[] = {0, 1, 2, 3, 4, 5, 6, 7, 8};
|
||||
const ::capnp::_::RawSchema s_f35cc4560bbf6ec2 = {
|
||||
0xf35cc4560bbf6ec2, b_f35cc4560bbf6ec2.words, 172, d_f35cc4560bbf6ec2, m_f35cc4560bbf6ec2,
|
||||
6, 8, i_f35cc4560bbf6ec2, nullptr, nullptr, { &s_f35cc4560bbf6ec2, nullptr, nullptr, 0, 0, nullptr }, false
|
||||
0xf35cc4560bbf6ec2, b_f35cc4560bbf6ec2.words, 192, d_f35cc4560bbf6ec2, m_f35cc4560bbf6ec2,
|
||||
7, 9, i_f35cc4560bbf6ec2, nullptr, nullptr, { &s_f35cc4560bbf6ec2, nullptr, nullptr, 0, 0, nullptr }, false
|
||||
};
|
||||
#endif // !CAPNP_LITE
|
||||
static const ::capnp::_::AlignedData<73> b_bc6a5bae100ebf77 = {
|
||||
static const ::capnp::_::AlignedData<137> b_bc6a5bae100ebf77 = {
|
||||
{ 0, 0, 0, 0, 5, 0, 6, 0,
|
||||
119, 191, 14, 16, 174, 91, 106, 188,
|
||||
32, 0, 0, 0, 1, 0, 1, 0,
|
||||
@@ -2098,7 +2119,7 @@ static const ::capnp::_::AlignedData<73> b_bc6a5bae100ebf77 = {
|
||||
21, 0, 0, 0, 218, 1, 0, 0,
|
||||
49, 0, 0, 0, 23, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
69, 0, 0, 0, 175, 0, 0, 0,
|
||||
69, 0, 0, 0, 143, 1, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
99, 117, 115, 116, 111, 109, 46, 99,
|
||||
@@ -2116,28 +2137,56 @@ static const ::capnp::_::AlignedData<73> b_bc6a5bae100ebf77 = {
|
||||
120, 112, 101, 114, 105, 109, 101, 110,
|
||||
116, 97, 108, 67, 111, 110, 116, 114,
|
||||
111, 108, 83, 116, 97, 116, 101, 0,
|
||||
12, 0, 0, 0, 3, 0, 4, 0,
|
||||
28, 0, 0, 0, 3, 0, 4, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 1, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
69, 0, 0, 0, 50, 0, 0, 0,
|
||||
181, 0, 0, 0, 50, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
64, 0, 0, 0, 3, 0, 1, 0,
|
||||
76, 0, 0, 0, 2, 0, 1, 0,
|
||||
176, 0, 0, 0, 3, 0, 1, 0,
|
||||
188, 0, 0, 0, 2, 0, 1, 0,
|
||||
1, 0, 0, 0, 16, 0, 0, 0,
|
||||
0, 0, 1, 0, 1, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
73, 0, 0, 0, 66, 0, 0, 0,
|
||||
185, 0, 0, 0, 66, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
68, 0, 0, 0, 3, 0, 1, 0,
|
||||
80, 0, 0, 0, 2, 0, 1, 0,
|
||||
180, 0, 0, 0, 3, 0, 1, 0,
|
||||
192, 0, 0, 0, 2, 0, 1, 0,
|
||||
2, 0, 0, 0, 17, 0, 0, 0,
|
||||
0, 0, 1, 0, 2, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
77, 0, 0, 0, 58, 0, 0, 0,
|
||||
189, 0, 0, 0, 58, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
72, 0, 0, 0, 3, 0, 1, 0,
|
||||
84, 0, 0, 0, 2, 0, 1, 0,
|
||||
184, 0, 0, 0, 3, 0, 1, 0,
|
||||
196, 0, 0, 0, 2, 0, 1, 0,
|
||||
3, 0, 0, 0, 1, 0, 0, 0,
|
||||
0, 0, 1, 0, 3, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
193, 0, 0, 0, 98, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
192, 0, 0, 0, 3, 0, 1, 0,
|
||||
204, 0, 0, 0, 2, 0, 1, 0,
|
||||
4, 0, 0, 0, 18, 0, 0, 0,
|
||||
0, 0, 1, 0, 4, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
201, 0, 0, 0, 114, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
200, 0, 0, 0, 3, 0, 1, 0,
|
||||
212, 0, 0, 0, 2, 0, 1, 0,
|
||||
5, 0, 0, 0, 19, 0, 0, 0,
|
||||
0, 0, 1, 0, 5, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
209, 0, 0, 0, 98, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
208, 0, 0, 0, 3, 0, 1, 0,
|
||||
220, 0, 0, 0, 2, 0, 1, 0,
|
||||
6, 0, 0, 0, 20, 0, 0, 0,
|
||||
0, 0, 1, 0, 6, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
217, 0, 0, 0, 74, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
216, 0, 0, 0, 3, 0, 1, 0,
|
||||
228, 0, 0, 0, 2, 0, 1, 0,
|
||||
115, 116, 97, 116, 101, 0, 0, 0,
|
||||
15, 0, 0, 0, 0, 0, 0, 0,
|
||||
55, 64, 112, 160, 167, 242, 246, 216,
|
||||
@@ -2155,6 +2204,42 @@ static const ::capnp::_::AlignedData<73> b_bc6a5bae100ebf77 = {
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
97, 99, 116, 105, 118, 101, 0, 0,
|
||||
1, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
100, 101, 99, 101, 108, 73, 110, 116,
|
||||
101, 110, 116, 0, 0, 0, 0, 0,
|
||||
10, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
10, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
99, 117, 114, 118, 101, 68, 101, 116,
|
||||
101, 99, 116, 101, 100, 0, 0, 0,
|
||||
1, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
119, 97, 110, 116, 66, 108, 101, 110,
|
||||
100, 101, 100, 0, 0, 0, 0, 0,
|
||||
1, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
108, 101, 97, 100, 86, 101, 116, 111,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
@@ -2168,11 +2253,11 @@ static const ::capnp::_::AlignedData<73> b_bc6a5bae100ebf77 = {
|
||||
static const ::capnp::_::RawSchema* const d_bc6a5bae100ebf77[] = {
|
||||
&s_d8f6f2a7a0704037,
|
||||
};
|
||||
static const uint16_t m_bc6a5bae100ebf77[] = {2, 1, 0};
|
||||
static const uint16_t i_bc6a5bae100ebf77[] = {0, 1, 2};
|
||||
static const uint16_t m_bc6a5bae100ebf77[] = {2, 4, 3, 1, 6, 0, 5};
|
||||
static const uint16_t i_bc6a5bae100ebf77[] = {0, 1, 2, 3, 4, 5, 6};
|
||||
const ::capnp::_::RawSchema s_bc6a5bae100ebf77 = {
|
||||
0xbc6a5bae100ebf77, b_bc6a5bae100ebf77.words, 73, d_bc6a5bae100ebf77, m_bc6a5bae100ebf77,
|
||||
1, 3, i_bc6a5bae100ebf77, nullptr, nullptr, { &s_bc6a5bae100ebf77, nullptr, nullptr, 0, 0, nullptr }, false
|
||||
0xbc6a5bae100ebf77, b_bc6a5bae100ebf77.words, 137, d_bc6a5bae100ebf77, m_bc6a5bae100ebf77,
|
||||
1, 7, i_bc6a5bae100ebf77, nullptr, nullptr, { &s_bc6a5bae100ebf77, nullptr, nullptr, 0, 0, nullptr }, false
|
||||
};
|
||||
#endif // !CAPNP_LITE
|
||||
static const ::capnp::_::AlignedData<34> b_d8f6f2a7a0704037 = {
|
||||
@@ -3257,6 +3342,132 @@ const ::capnp::_::RawSchema s_a567bce822ae28fe = {
|
||||
0, 2, i_a567bce822ae28fe, nullptr, nullptr, { &s_a567bce822ae28fe, nullptr, nullptr, 0, 0, nullptr }, false
|
||||
};
|
||||
#endif // !CAPNP_LITE
|
||||
static const ::capnp::_::AlignedData<68> b_88dcd6d006f3c4a1 = {
|
||||
{ 0, 0, 0, 0, 5, 0, 6, 0,
|
||||
161, 196, 243, 6, 208, 214, 220, 136,
|
||||
32, 0, 0, 0, 1, 0, 1, 0,
|
||||
194, 110, 191, 11, 86, 196, 92, 243,
|
||||
0, 0, 7, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
21, 0, 0, 0, 130, 1, 0, 0,
|
||||
41, 0, 0, 0, 23, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
49, 0, 0, 0, 175, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
99, 117, 115, 116, 111, 109, 46, 99,
|
||||
97, 112, 110, 112, 58, 76, 111, 110,
|
||||
103, 105, 116, 117, 100, 105, 110, 97,
|
||||
108, 80, 108, 97, 110, 83, 80, 46,
|
||||
65, 99, 99, 101, 108, 67, 111, 110,
|
||||
116, 114, 111, 108, 108, 101, 114, 0,
|
||||
4, 0, 0, 0, 1, 0, 1, 0,
|
||||
166, 80, 70, 176, 68, 149, 159, 140,
|
||||
1, 0, 0, 0, 66, 0, 0, 0,
|
||||
80, 114, 111, 102, 105, 108, 101, 0,
|
||||
12, 0, 0, 0, 3, 0, 4, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 1, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
69, 0, 0, 0, 66, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
64, 0, 0, 0, 3, 0, 1, 0,
|
||||
76, 0, 0, 0, 2, 0, 1, 0,
|
||||
1, 0, 0, 0, 1, 0, 0, 0,
|
||||
0, 0, 1, 0, 1, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
73, 0, 0, 0, 58, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
68, 0, 0, 0, 3, 0, 1, 0,
|
||||
80, 0, 0, 0, 2, 0, 1, 0,
|
||||
2, 0, 0, 0, 1, 0, 0, 0,
|
||||
0, 0, 1, 0, 2, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
77, 0, 0, 0, 66, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
72, 0, 0, 0, 3, 0, 1, 0,
|
||||
84, 0, 0, 0, 2, 0, 1, 0,
|
||||
101, 110, 97, 98, 108, 101, 100, 0,
|
||||
1, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
97, 99, 116, 105, 118, 101, 0, 0,
|
||||
1, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
112, 114, 111, 102, 105, 108, 101, 0,
|
||||
15, 0, 0, 0, 0, 0, 0, 0,
|
||||
166, 80, 70, 176, 68, 149, 159, 140,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
15, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, }
|
||||
};
|
||||
::capnp::word const* const bp_88dcd6d006f3c4a1 = b_88dcd6d006f3c4a1.words;
|
||||
#if !CAPNP_LITE
|
||||
static const ::capnp::_::RawSchema* const d_88dcd6d006f3c4a1[] = {
|
||||
&s_8c9f9544b04650a6,
|
||||
};
|
||||
static const uint16_t m_88dcd6d006f3c4a1[] = {1, 0, 2};
|
||||
static const uint16_t i_88dcd6d006f3c4a1[] = {0, 1, 2};
|
||||
const ::capnp::_::RawSchema s_88dcd6d006f3c4a1 = {
|
||||
0x88dcd6d006f3c4a1, b_88dcd6d006f3c4a1.words, 68, d_88dcd6d006f3c4a1, m_88dcd6d006f3c4a1,
|
||||
1, 3, i_88dcd6d006f3c4a1, nullptr, nullptr, { &s_88dcd6d006f3c4a1, nullptr, nullptr, 0, 0, nullptr }, false
|
||||
};
|
||||
#endif // !CAPNP_LITE
|
||||
static const ::capnp::_::AlignedData<33> b_8c9f9544b04650a6 = {
|
||||
{ 0, 0, 0, 0, 5, 0, 6, 0,
|
||||
166, 80, 70, 176, 68, 149, 159, 140,
|
||||
48, 0, 0, 0, 2, 0, 0, 0,
|
||||
161, 196, 243, 6, 208, 214, 220, 136,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
21, 0, 0, 0, 194, 1, 0, 0,
|
||||
45, 0, 0, 0, 7, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
41, 0, 0, 0, 79, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
99, 117, 115, 116, 111, 109, 46, 99,
|
||||
97, 112, 110, 112, 58, 76, 111, 110,
|
||||
103, 105, 116, 117, 100, 105, 110, 97,
|
||||
108, 80, 108, 97, 110, 83, 80, 46,
|
||||
65, 99, 99, 101, 108, 67, 111, 110,
|
||||
116, 114, 111, 108, 108, 101, 114, 46,
|
||||
80, 114, 111, 102, 105, 108, 101, 0,
|
||||
0, 0, 0, 0, 1, 0, 1, 0,
|
||||
12, 0, 0, 0, 1, 0, 2, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
29, 0, 0, 0, 34, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 0, 0, 0, 0, 0, 0, 0,
|
||||
21, 0, 0, 0, 58, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
2, 0, 0, 0, 0, 0, 0, 0,
|
||||
13, 0, 0, 0, 50, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
101, 99, 111, 0, 0, 0, 0, 0,
|
||||
110, 111, 114, 109, 97, 108, 0, 0,
|
||||
115, 112, 111, 114, 116, 0, 0, 0, }
|
||||
};
|
||||
::capnp::word const* const bp_8c9f9544b04650a6 = b_8c9f9544b04650a6.words;
|
||||
#if !CAPNP_LITE
|
||||
static const uint16_t m_8c9f9544b04650a6[] = {0, 1, 2};
|
||||
const ::capnp::_::RawSchema s_8c9f9544b04650a6 = {
|
||||
0x8c9f9544b04650a6, b_8c9f9544b04650a6.words, 33, nullptr, m_8c9f9544b04650a6,
|
||||
0, 3, nullptr, nullptr, nullptr, { &s_8c9f9544b04650a6, nullptr, nullptr, 0, 0, nullptr }, false
|
||||
};
|
||||
#endif // !CAPNP_LITE
|
||||
CAPNP_DEFINE_ENUM(Profile_8c9f9544b04650a6, 8c9f9544b04650a6);
|
||||
static const ::capnp::_::AlignedData<44> b_da96579883444c35 = {
|
||||
{ 0, 0, 0, 0, 5, 0, 6, 0,
|
||||
53, 76, 68, 131, 152, 87, 150, 218,
|
||||
@@ -4812,33 +5023,65 @@ const ::capnp::_::RawSchema s_9e62278160b7df26 = {
|
||||
2, 8, i_9e62278160b7df26, nullptr, nullptr, { &s_9e62278160b7df26, nullptr, nullptr, 0, 0, nullptr }, false
|
||||
};
|
||||
#endif // !CAPNP_LITE
|
||||
static const ::capnp::_::AlignedData<33> b_b86e6369214c01c8 = {
|
||||
static const ::capnp::_::AlignedData<65> b_b86e6369214c01c8 = {
|
||||
{ 0, 0, 0, 0, 5, 0, 6, 0,
|
||||
200, 1, 76, 33, 105, 99, 110, 184,
|
||||
13, 0, 0, 0, 1, 0, 1, 0,
|
||||
13, 0, 0, 0, 1, 0, 2, 0,
|
||||
89, 10, 85, 29, 102, 186, 38, 181,
|
||||
0, 0, 7, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
21, 0, 0, 0, 194, 0, 0, 0,
|
||||
29, 0, 0, 0, 7, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
25, 0, 0, 0, 63, 0, 0, 0,
|
||||
25, 0, 0, 0, 175, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
99, 117, 115, 116, 111, 109, 46, 99,
|
||||
97, 112, 110, 112, 58, 67, 97, 114,
|
||||
83, 116, 97, 116, 101, 83, 80, 0,
|
||||
0, 0, 0, 0, 1, 0, 1, 0,
|
||||
4, 0, 0, 0, 3, 0, 4, 0,
|
||||
12, 0, 0, 0, 3, 0, 4, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 1, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
13, 0, 0, 0, 90, 0, 0, 0,
|
||||
69, 0, 0, 0, 90, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
12, 0, 0, 0, 3, 0, 1, 0,
|
||||
24, 0, 0, 0, 2, 0, 1, 0,
|
||||
68, 0, 0, 0, 3, 0, 1, 0,
|
||||
80, 0, 0, 0, 2, 0, 1, 0,
|
||||
1, 0, 0, 0, 32, 0, 0, 0,
|
||||
0, 0, 1, 0, 1, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
77, 0, 0, 0, 82, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
76, 0, 0, 0, 3, 0, 1, 0,
|
||||
88, 0, 0, 0, 2, 0, 1, 0,
|
||||
2, 0, 0, 0, 2, 0, 0, 0,
|
||||
0, 0, 1, 0, 2, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
85, 0, 0, 0, 82, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
84, 0, 0, 0, 3, 0, 1, 0,
|
||||
96, 0, 0, 0, 2, 0, 1, 0,
|
||||
115, 112, 101, 101, 100, 76, 105, 109,
|
||||
105, 116, 0, 0, 0, 0, 0, 0,
|
||||
10, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
10, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
101, 110, 103, 105, 110, 101, 79, 102,
|
||||
102, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
101, 110, 103, 105, 110, 101, 82, 112,
|
||||
109, 0, 0, 0, 0, 0, 0, 0,
|
||||
10, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
@@ -4849,11 +5092,11 @@ static const ::capnp::_::AlignedData<33> b_b86e6369214c01c8 = {
|
||||
};
|
||||
::capnp::word const* const bp_b86e6369214c01c8 = b_b86e6369214c01c8.words;
|
||||
#if !CAPNP_LITE
|
||||
static const uint16_t m_b86e6369214c01c8[] = {0};
|
||||
static const uint16_t i_b86e6369214c01c8[] = {0};
|
||||
static const uint16_t m_b86e6369214c01c8[] = {1, 2, 0};
|
||||
static const uint16_t i_b86e6369214c01c8[] = {0, 1, 2};
|
||||
const ::capnp::_::RawSchema s_b86e6369214c01c8 = {
|
||||
0xb86e6369214c01c8, b_b86e6369214c01c8.words, 33, nullptr, m_b86e6369214c01c8,
|
||||
0, 1, i_b86e6369214c01c8, nullptr, nullptr, { &s_b86e6369214c01c8, nullptr, nullptr, 0, 0, nullptr }, false
|
||||
0xb86e6369214c01c8, b_b86e6369214c01c8.words, 65, nullptr, m_b86e6369214c01c8,
|
||||
0, 3, i_b86e6369214c01c8, nullptr, nullptr, { &s_b86e6369214c01c8, nullptr, nullptr, 0, 0, nullptr }, false
|
||||
};
|
||||
#endif // !CAPNP_LITE
|
||||
static const ::capnp::_::AlignedData<116> b_f416ec09499d9d19 = {
|
||||
@@ -5635,6 +5878,18 @@ constexpr ::capnp::_::RawSchema const* LongitudinalPlanSP::E2eAlerts::_capnpPriv
|
||||
#endif // !CAPNP_NEED_REDUNDANT_CONSTEXPR_DECL
|
||||
#endif // !CAPNP_LITE
|
||||
|
||||
// LongitudinalPlanSP::AccelController
|
||||
#if CAPNP_NEED_REDUNDANT_CONSTEXPR_DECL
|
||||
constexpr uint16_t LongitudinalPlanSP::AccelController::_capnpPrivate::dataWordSize;
|
||||
constexpr uint16_t LongitudinalPlanSP::AccelController::_capnpPrivate::pointerCount;
|
||||
#endif // !CAPNP_NEED_REDUNDANT_CONSTEXPR_DECL
|
||||
#if !CAPNP_LITE
|
||||
#if CAPNP_NEED_REDUNDANT_CONSTEXPR_DECL
|
||||
constexpr ::capnp::Kind LongitudinalPlanSP::AccelController::_capnpPrivate::kind;
|
||||
constexpr ::capnp::_::RawSchema const* LongitudinalPlanSP::AccelController::_capnpPrivate::schema;
|
||||
#endif // !CAPNP_NEED_REDUNDANT_CONSTEXPR_DECL
|
||||
#endif // !CAPNP_LITE
|
||||
|
||||
// OnroadEventSP
|
||||
#if CAPNP_NEED_REDUNDANT_CONSTEXPR_DECL
|
||||
constexpr uint16_t OnroadEventSP::_capnpPrivate::dataWordSize;
|
||||
|
||||
@@ -178,6 +178,14 @@ enum class LongitudinalPlanSource_ad47440556d96ec4: uint16_t {
|
||||
};
|
||||
CAPNP_DECLARE_ENUM(LongitudinalPlanSource, ad47440556d96ec4);
|
||||
CAPNP_DECLARE_SCHEMA(a567bce822ae28fe);
|
||||
CAPNP_DECLARE_SCHEMA(88dcd6d006f3c4a1);
|
||||
CAPNP_DECLARE_SCHEMA(8c9f9544b04650a6);
|
||||
enum class Profile_8c9f9544b04650a6: uint16_t {
|
||||
ECO,
|
||||
NORMAL,
|
||||
SPORT,
|
||||
};
|
||||
CAPNP_DECLARE_ENUM(Profile, 8c9f9544b04650a6);
|
||||
CAPNP_DECLARE_SCHEMA(da96579883444c35);
|
||||
CAPNP_DECLARE_SCHEMA(f6e831752fcdf793);
|
||||
CAPNP_DECLARE_SCHEMA(b8007ed8a646b5e6);
|
||||
@@ -477,9 +485,10 @@ struct LongitudinalPlanSP {
|
||||
typedef ::capnp::schemas::LongitudinalPlanSource_ad47440556d96ec4 LongitudinalPlanSource;
|
||||
|
||||
struct E2eAlerts;
|
||||
struct AccelController;
|
||||
|
||||
struct _capnpPrivate {
|
||||
CAPNP_DECLARE_STRUCT_HEADER(f35cc4560bbf6ec2, 2, 5)
|
||||
CAPNP_DECLARE_STRUCT_HEADER(f35cc4560bbf6ec2, 2, 6)
|
||||
#if !CAPNP_LITE
|
||||
static constexpr ::capnp::_::RawBrandedSchema const* brand() { return &schema->defaultBrand; }
|
||||
#endif // !CAPNP_LITE
|
||||
@@ -620,6 +629,23 @@ struct LongitudinalPlanSP::E2eAlerts {
|
||||
};
|
||||
};
|
||||
|
||||
struct LongitudinalPlanSP::AccelController {
|
||||
AccelController() = delete;
|
||||
|
||||
class Reader;
|
||||
class Builder;
|
||||
class Pipeline;
|
||||
typedef ::capnp::schemas::Profile_8c9f9544b04650a6 Profile;
|
||||
|
||||
|
||||
struct _capnpPrivate {
|
||||
CAPNP_DECLARE_STRUCT_HEADER(88dcd6d006f3c4a1, 1, 0)
|
||||
#if !CAPNP_LITE
|
||||
static constexpr ::capnp::_::RawBrandedSchema const* brand() { return &schema->defaultBrand; }
|
||||
#endif // !CAPNP_LITE
|
||||
};
|
||||
};
|
||||
|
||||
struct OnroadEventSP {
|
||||
OnroadEventSP() = delete;
|
||||
|
||||
@@ -806,7 +832,7 @@ struct CarStateSP {
|
||||
class Pipeline;
|
||||
|
||||
struct _capnpPrivate {
|
||||
CAPNP_DECLARE_STRUCT_HEADER(b86e6369214c01c8, 1, 0)
|
||||
CAPNP_DECLARE_STRUCT_HEADER(b86e6369214c01c8, 2, 0)
|
||||
#if !CAPNP_LITE
|
||||
static constexpr ::capnp::_::RawBrandedSchema const* brand() { return &schema->defaultBrand; }
|
||||
#endif // !CAPNP_LITE
|
||||
@@ -2300,6 +2326,9 @@ public:
|
||||
inline bool hasE2eAlerts() const;
|
||||
inline ::cereal::LongitudinalPlanSP::E2eAlerts::Reader getE2eAlerts() const;
|
||||
|
||||
inline bool hasAccelController() const;
|
||||
inline ::cereal::LongitudinalPlanSP::AccelController::Reader getAccelController() const;
|
||||
|
||||
private:
|
||||
::capnp::_::StructReader _reader;
|
||||
template <typename, ::capnp::Kind>
|
||||
@@ -2372,6 +2401,13 @@ public:
|
||||
inline void adoptE2eAlerts(::capnp::Orphan< ::cereal::LongitudinalPlanSP::E2eAlerts>&& value);
|
||||
inline ::capnp::Orphan< ::cereal::LongitudinalPlanSP::E2eAlerts> disownE2eAlerts();
|
||||
|
||||
inline bool hasAccelController();
|
||||
inline ::cereal::LongitudinalPlanSP::AccelController::Builder getAccelController();
|
||||
inline void setAccelController( ::cereal::LongitudinalPlanSP::AccelController::Reader value);
|
||||
inline ::cereal::LongitudinalPlanSP::AccelController::Builder initAccelController();
|
||||
inline void adoptAccelController(::capnp::Orphan< ::cereal::LongitudinalPlanSP::AccelController>&& value);
|
||||
inline ::capnp::Orphan< ::cereal::LongitudinalPlanSP::AccelController> disownAccelController();
|
||||
|
||||
private:
|
||||
::capnp::_::StructBuilder _builder;
|
||||
template <typename, ::capnp::Kind>
|
||||
@@ -2394,6 +2430,7 @@ public:
|
||||
inline ::cereal::LongitudinalPlanSP::SmartCruiseControl::Pipeline getSmartCruiseControl();
|
||||
inline ::cereal::LongitudinalPlanSP::SpeedLimit::Pipeline getSpeedLimit();
|
||||
inline ::cereal::LongitudinalPlanSP::E2eAlerts::Pipeline getE2eAlerts();
|
||||
inline ::cereal::LongitudinalPlanSP::AccelController::Pipeline getAccelController();
|
||||
private:
|
||||
::capnp::AnyPointer::Pipeline _typeless;
|
||||
friend class ::capnp::PipelineHook;
|
||||
@@ -2425,6 +2462,14 @@ public:
|
||||
|
||||
inline bool getActive() const;
|
||||
|
||||
inline float getDecelIntent() const;
|
||||
|
||||
inline bool getCurveDetected() const;
|
||||
|
||||
inline bool getWantBlended() const;
|
||||
|
||||
inline bool getLeadVeto() const;
|
||||
|
||||
private:
|
||||
::capnp::_::StructReader _reader;
|
||||
template <typename, ::capnp::Kind>
|
||||
@@ -2462,6 +2507,18 @@ public:
|
||||
inline bool getActive();
|
||||
inline void setActive(bool value);
|
||||
|
||||
inline float getDecelIntent();
|
||||
inline void setDecelIntent(float value);
|
||||
|
||||
inline bool getCurveDetected();
|
||||
inline void setCurveDetected(bool value);
|
||||
|
||||
inline bool getWantBlended();
|
||||
inline void setWantBlended(bool value);
|
||||
|
||||
inline bool getLeadVeto();
|
||||
inline void setLeadVeto(bool value);
|
||||
|
||||
private:
|
||||
::capnp::_::StructBuilder _builder;
|
||||
template <typename, ::capnp::Kind>
|
||||
@@ -3169,6 +3226,92 @@ private:
|
||||
};
|
||||
#endif // !CAPNP_LITE
|
||||
|
||||
class LongitudinalPlanSP::AccelController::Reader {
|
||||
public:
|
||||
typedef AccelController Reads;
|
||||
|
||||
Reader() = default;
|
||||
inline explicit Reader(::capnp::_::StructReader base): _reader(base) {}
|
||||
|
||||
inline ::capnp::MessageSize totalSize() const {
|
||||
return _reader.totalSize().asPublic();
|
||||
}
|
||||
|
||||
#if !CAPNP_LITE
|
||||
inline ::kj::StringTree toString() const {
|
||||
return ::capnp::_::structString(_reader, *_capnpPrivate::brand());
|
||||
}
|
||||
#endif // !CAPNP_LITE
|
||||
|
||||
inline bool getEnabled() const;
|
||||
|
||||
inline bool getActive() const;
|
||||
|
||||
inline ::cereal::LongitudinalPlanSP::AccelController::Profile getProfile() const;
|
||||
|
||||
private:
|
||||
::capnp::_::StructReader _reader;
|
||||
template <typename, ::capnp::Kind>
|
||||
friend struct ::capnp::ToDynamic_;
|
||||
template <typename, ::capnp::Kind>
|
||||
friend struct ::capnp::_::PointerHelpers;
|
||||
template <typename, ::capnp::Kind>
|
||||
friend struct ::capnp::List;
|
||||
friend class ::capnp::MessageBuilder;
|
||||
friend class ::capnp::Orphanage;
|
||||
};
|
||||
|
||||
class LongitudinalPlanSP::AccelController::Builder {
|
||||
public:
|
||||
typedef AccelController Builds;
|
||||
|
||||
Builder() = delete; // Deleted to discourage incorrect usage.
|
||||
// You can explicitly initialize to nullptr instead.
|
||||
inline Builder(decltype(nullptr)) {}
|
||||
inline explicit Builder(::capnp::_::StructBuilder base): _builder(base) {}
|
||||
inline operator Reader() const { return Reader(_builder.asReader()); }
|
||||
inline Reader asReader() const { return *this; }
|
||||
|
||||
inline ::capnp::MessageSize totalSize() const { return asReader().totalSize(); }
|
||||
#if !CAPNP_LITE
|
||||
inline ::kj::StringTree toString() const { return asReader().toString(); }
|
||||
#endif // !CAPNP_LITE
|
||||
|
||||
inline bool getEnabled();
|
||||
inline void setEnabled(bool value);
|
||||
|
||||
inline bool getActive();
|
||||
inline void setActive(bool value);
|
||||
|
||||
inline ::cereal::LongitudinalPlanSP::AccelController::Profile getProfile();
|
||||
inline void setProfile( ::cereal::LongitudinalPlanSP::AccelController::Profile value);
|
||||
|
||||
private:
|
||||
::capnp::_::StructBuilder _builder;
|
||||
template <typename, ::capnp::Kind>
|
||||
friend struct ::capnp::ToDynamic_;
|
||||
friend class ::capnp::Orphanage;
|
||||
template <typename, ::capnp::Kind>
|
||||
friend struct ::capnp::_::PointerHelpers;
|
||||
};
|
||||
|
||||
#if !CAPNP_LITE
|
||||
class LongitudinalPlanSP::AccelController::Pipeline {
|
||||
public:
|
||||
typedef AccelController Pipelines;
|
||||
|
||||
inline Pipeline(decltype(nullptr)): _typeless(nullptr) {}
|
||||
inline explicit Pipeline(::capnp::AnyPointer::Pipeline&& typeless)
|
||||
: _typeless(kj::mv(typeless)) {}
|
||||
|
||||
private:
|
||||
::capnp::AnyPointer::Pipeline _typeless;
|
||||
friend class ::capnp::PipelineHook;
|
||||
template <typename, ::capnp::Kind>
|
||||
friend struct ::capnp::ToDynamic_;
|
||||
};
|
||||
#endif // !CAPNP_LITE
|
||||
|
||||
class OnroadEventSP::Reader {
|
||||
public:
|
||||
typedef OnroadEventSP Reads;
|
||||
@@ -4378,6 +4521,10 @@ public:
|
||||
|
||||
inline float getSpeedLimit() const;
|
||||
|
||||
inline bool getEngineOff() const;
|
||||
|
||||
inline float getEngineRpm() const;
|
||||
|
||||
private:
|
||||
::capnp::_::StructReader _reader;
|
||||
template <typename, ::capnp::Kind>
|
||||
@@ -4409,6 +4556,12 @@ public:
|
||||
inline float getSpeedLimit();
|
||||
inline void setSpeedLimit(float value);
|
||||
|
||||
inline bool getEngineOff();
|
||||
inline void setEngineOff(bool value);
|
||||
|
||||
inline float getEngineRpm();
|
||||
inline void setEngineRpm(float value);
|
||||
|
||||
private:
|
||||
::capnp::_::StructBuilder _builder;
|
||||
template <typename, ::capnp::Kind>
|
||||
@@ -6883,6 +7036,45 @@ inline ::capnp::Orphan< ::cereal::LongitudinalPlanSP::E2eAlerts> LongitudinalPla
|
||||
::capnp::bounded<4>() * ::capnp::POINTERS));
|
||||
}
|
||||
|
||||
inline bool LongitudinalPlanSP::Reader::hasAccelController() const {
|
||||
return !_reader.getPointerField(
|
||||
::capnp::bounded<5>() * ::capnp::POINTERS).isNull();
|
||||
}
|
||||
inline bool LongitudinalPlanSP::Builder::hasAccelController() {
|
||||
return !_builder.getPointerField(
|
||||
::capnp::bounded<5>() * ::capnp::POINTERS).isNull();
|
||||
}
|
||||
inline ::cereal::LongitudinalPlanSP::AccelController::Reader LongitudinalPlanSP::Reader::getAccelController() const {
|
||||
return ::capnp::_::PointerHelpers< ::cereal::LongitudinalPlanSP::AccelController>::get(_reader.getPointerField(
|
||||
::capnp::bounded<5>() * ::capnp::POINTERS));
|
||||
}
|
||||
inline ::cereal::LongitudinalPlanSP::AccelController::Builder LongitudinalPlanSP::Builder::getAccelController() {
|
||||
return ::capnp::_::PointerHelpers< ::cereal::LongitudinalPlanSP::AccelController>::get(_builder.getPointerField(
|
||||
::capnp::bounded<5>() * ::capnp::POINTERS));
|
||||
}
|
||||
#if !CAPNP_LITE
|
||||
inline ::cereal::LongitudinalPlanSP::AccelController::Pipeline LongitudinalPlanSP::Pipeline::getAccelController() {
|
||||
return ::cereal::LongitudinalPlanSP::AccelController::Pipeline(_typeless.getPointerField(5));
|
||||
}
|
||||
#endif // !CAPNP_LITE
|
||||
inline void LongitudinalPlanSP::Builder::setAccelController( ::cereal::LongitudinalPlanSP::AccelController::Reader value) {
|
||||
::capnp::_::PointerHelpers< ::cereal::LongitudinalPlanSP::AccelController>::set(_builder.getPointerField(
|
||||
::capnp::bounded<5>() * ::capnp::POINTERS), value);
|
||||
}
|
||||
inline ::cereal::LongitudinalPlanSP::AccelController::Builder LongitudinalPlanSP::Builder::initAccelController() {
|
||||
return ::capnp::_::PointerHelpers< ::cereal::LongitudinalPlanSP::AccelController>::init(_builder.getPointerField(
|
||||
::capnp::bounded<5>() * ::capnp::POINTERS));
|
||||
}
|
||||
inline void LongitudinalPlanSP::Builder::adoptAccelController(
|
||||
::capnp::Orphan< ::cereal::LongitudinalPlanSP::AccelController>&& value) {
|
||||
::capnp::_::PointerHelpers< ::cereal::LongitudinalPlanSP::AccelController>::adopt(_builder.getPointerField(
|
||||
::capnp::bounded<5>() * ::capnp::POINTERS), kj::mv(value));
|
||||
}
|
||||
inline ::capnp::Orphan< ::cereal::LongitudinalPlanSP::AccelController> LongitudinalPlanSP::Builder::disownAccelController() {
|
||||
return ::capnp::_::PointerHelpers< ::cereal::LongitudinalPlanSP::AccelController>::disown(_builder.getPointerField(
|
||||
::capnp::bounded<5>() * ::capnp::POINTERS));
|
||||
}
|
||||
|
||||
inline ::cereal::LongitudinalPlanSP::DynamicExperimentalControl::DynamicExperimentalControlState LongitudinalPlanSP::DynamicExperimentalControl::Reader::getState() const {
|
||||
return _reader.getDataField< ::cereal::LongitudinalPlanSP::DynamicExperimentalControl::DynamicExperimentalControlState>(
|
||||
::capnp::bounded<0>() * ::capnp::ELEMENTS);
|
||||
@@ -6925,6 +7117,62 @@ inline void LongitudinalPlanSP::DynamicExperimentalControl::Builder::setActive(b
|
||||
::capnp::bounded<17>() * ::capnp::ELEMENTS, value);
|
||||
}
|
||||
|
||||
inline float LongitudinalPlanSP::DynamicExperimentalControl::Reader::getDecelIntent() const {
|
||||
return _reader.getDataField<float>(
|
||||
::capnp::bounded<1>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
|
||||
inline float LongitudinalPlanSP::DynamicExperimentalControl::Builder::getDecelIntent() {
|
||||
return _builder.getDataField<float>(
|
||||
::capnp::bounded<1>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
inline void LongitudinalPlanSP::DynamicExperimentalControl::Builder::setDecelIntent(float value) {
|
||||
_builder.setDataField<float>(
|
||||
::capnp::bounded<1>() * ::capnp::ELEMENTS, value);
|
||||
}
|
||||
|
||||
inline bool LongitudinalPlanSP::DynamicExperimentalControl::Reader::getCurveDetected() const {
|
||||
return _reader.getDataField<bool>(
|
||||
::capnp::bounded<18>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
|
||||
inline bool LongitudinalPlanSP::DynamicExperimentalControl::Builder::getCurveDetected() {
|
||||
return _builder.getDataField<bool>(
|
||||
::capnp::bounded<18>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
inline void LongitudinalPlanSP::DynamicExperimentalControl::Builder::setCurveDetected(bool value) {
|
||||
_builder.setDataField<bool>(
|
||||
::capnp::bounded<18>() * ::capnp::ELEMENTS, value);
|
||||
}
|
||||
|
||||
inline bool LongitudinalPlanSP::DynamicExperimentalControl::Reader::getWantBlended() const {
|
||||
return _reader.getDataField<bool>(
|
||||
::capnp::bounded<19>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
|
||||
inline bool LongitudinalPlanSP::DynamicExperimentalControl::Builder::getWantBlended() {
|
||||
return _builder.getDataField<bool>(
|
||||
::capnp::bounded<19>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
inline void LongitudinalPlanSP::DynamicExperimentalControl::Builder::setWantBlended(bool value) {
|
||||
_builder.setDataField<bool>(
|
||||
::capnp::bounded<19>() * ::capnp::ELEMENTS, value);
|
||||
}
|
||||
|
||||
inline bool LongitudinalPlanSP::DynamicExperimentalControl::Reader::getLeadVeto() const {
|
||||
return _reader.getDataField<bool>(
|
||||
::capnp::bounded<20>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
|
||||
inline bool LongitudinalPlanSP::DynamicExperimentalControl::Builder::getLeadVeto() {
|
||||
return _builder.getDataField<bool>(
|
||||
::capnp::bounded<20>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
inline void LongitudinalPlanSP::DynamicExperimentalControl::Builder::setLeadVeto(bool value) {
|
||||
_builder.setDataField<bool>(
|
||||
::capnp::bounded<20>() * ::capnp::ELEMENTS, value);
|
||||
}
|
||||
|
||||
inline bool LongitudinalPlanSP::SmartCruiseControl::Reader::hasVision() const {
|
||||
return !_reader.getPointerField(
|
||||
::capnp::bounded<0>() * ::capnp::POINTERS).isNull();
|
||||
@@ -7473,6 +7721,48 @@ inline void LongitudinalPlanSP::E2eAlerts::Builder::setLeadDepartAlert(bool valu
|
||||
::capnp::bounded<1>() * ::capnp::ELEMENTS, value);
|
||||
}
|
||||
|
||||
inline bool LongitudinalPlanSP::AccelController::Reader::getEnabled() const {
|
||||
return _reader.getDataField<bool>(
|
||||
::capnp::bounded<0>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
|
||||
inline bool LongitudinalPlanSP::AccelController::Builder::getEnabled() {
|
||||
return _builder.getDataField<bool>(
|
||||
::capnp::bounded<0>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
inline void LongitudinalPlanSP::AccelController::Builder::setEnabled(bool value) {
|
||||
_builder.setDataField<bool>(
|
||||
::capnp::bounded<0>() * ::capnp::ELEMENTS, value);
|
||||
}
|
||||
|
||||
inline bool LongitudinalPlanSP::AccelController::Reader::getActive() const {
|
||||
return _reader.getDataField<bool>(
|
||||
::capnp::bounded<1>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
|
||||
inline bool LongitudinalPlanSP::AccelController::Builder::getActive() {
|
||||
return _builder.getDataField<bool>(
|
||||
::capnp::bounded<1>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
inline void LongitudinalPlanSP::AccelController::Builder::setActive(bool value) {
|
||||
_builder.setDataField<bool>(
|
||||
::capnp::bounded<1>() * ::capnp::ELEMENTS, value);
|
||||
}
|
||||
|
||||
inline ::cereal::LongitudinalPlanSP::AccelController::Profile LongitudinalPlanSP::AccelController::Reader::getProfile() const {
|
||||
return _reader.getDataField< ::cereal::LongitudinalPlanSP::AccelController::Profile>(
|
||||
::capnp::bounded<1>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
|
||||
inline ::cereal::LongitudinalPlanSP::AccelController::Profile LongitudinalPlanSP::AccelController::Builder::getProfile() {
|
||||
return _builder.getDataField< ::cereal::LongitudinalPlanSP::AccelController::Profile>(
|
||||
::capnp::bounded<1>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
inline void LongitudinalPlanSP::AccelController::Builder::setProfile( ::cereal::LongitudinalPlanSP::AccelController::Profile value) {
|
||||
_builder.setDataField< ::cereal::LongitudinalPlanSP::AccelController::Profile>(
|
||||
::capnp::bounded<1>() * ::capnp::ELEMENTS, value);
|
||||
}
|
||||
|
||||
inline bool OnroadEventSP::Reader::hasEvents() const {
|
||||
return !_reader.getPointerField(
|
||||
::capnp::bounded<0>() * ::capnp::POINTERS).isNull();
|
||||
@@ -8807,6 +9097,34 @@ inline void CarStateSP::Builder::setSpeedLimit(float value) {
|
||||
::capnp::bounded<0>() * ::capnp::ELEMENTS, value);
|
||||
}
|
||||
|
||||
inline bool CarStateSP::Reader::getEngineOff() const {
|
||||
return _reader.getDataField<bool>(
|
||||
::capnp::bounded<32>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
|
||||
inline bool CarStateSP::Builder::getEngineOff() {
|
||||
return _builder.getDataField<bool>(
|
||||
::capnp::bounded<32>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
inline void CarStateSP::Builder::setEngineOff(bool value) {
|
||||
_builder.setDataField<bool>(
|
||||
::capnp::bounded<32>() * ::capnp::ELEMENTS, value);
|
||||
}
|
||||
|
||||
inline float CarStateSP::Reader::getEngineRpm() const {
|
||||
return _reader.getDataField<float>(
|
||||
::capnp::bounded<2>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
|
||||
inline float CarStateSP::Builder::getEngineRpm() {
|
||||
return _builder.getDataField<float>(
|
||||
::capnp::bounded<2>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
inline void CarStateSP::Builder::setEngineRpm(float value) {
|
||||
_builder.setDataField<float>(
|
||||
::capnp::bounded<2>() * ::capnp::ELEMENTS, value);
|
||||
}
|
||||
|
||||
inline bool LiveMapDataSP::Reader::getSpeedLimitValid() const {
|
||||
return _reader.getDataField<bool>(
|
||||
::capnp::bounded<0>() * ::capnp::ELEMENTS);
|
||||
|
||||
Binary file not shown.
@@ -192,8 +192,16 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"ShowAdvancedControls", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ShowTurnSignals", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"StandstillTimer", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"SunnypilotStoppingController", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"TrueVEgoUI", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
// toyota specific params
|
||||
{"ToyotaAutoHold", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ToyotaBrakeOnset", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ToyotaEnhancedBsm", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ToyotaTSS2Long", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ToyotaDriveMode", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
// MADS params
|
||||
{"Mads", {PERSISTENT | BACKUP, BOOL, "1"}},
|
||||
{"MadsMainCruiseAllowed", {PERSISTENT | BACKUP, BOOL, "1"}},
|
||||
@@ -243,6 +251,10 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"DynamicExperimentalControl", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"BlindSpot", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
// Accel Controller profiles (Eco / Normal / Sport)
|
||||
{"AccelPersonalityEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"AccelPersonality", {PERSISTENT | BACKUP, INT, "1"}},
|
||||
|
||||
// sunnypilot model params
|
||||
{"CameraOffset", {PERSISTENT | BACKUP, FLOAT, "0.0"}},
|
||||
{"LagdToggle", {PERSISTENT | BACKUP, BOOL, "1"}},
|
||||
|
||||
@@ -117,12 +117,16 @@ class TestParams(OpenpilotTestCase):
|
||||
def test_params_default_value(self):
|
||||
self.params.remove("LanguageSetting")
|
||||
self.params.remove("LongitudinalPersonality")
|
||||
self.params.remove("AccelPersonalityEnabled")
|
||||
self.params.remove("AccelPersonality")
|
||||
self.params.remove("LiveParametersV2")
|
||||
|
||||
assert self.params.get("LanguageSetting") is None
|
||||
assert self.params.get("LanguageSetting", return_default=False) is None
|
||||
assert isinstance(self.params.get("LanguageSetting", return_default=True), str)
|
||||
assert isinstance(self.params.get("LongitudinalPersonality", return_default=True), int)
|
||||
assert self.params.get("AccelPersonalityEnabled", return_default=True) is False
|
||||
assert self.params.get("AccelPersonality", return_default=True) == 1
|
||||
assert self.params.get("LiveParametersV2") is None
|
||||
assert self.params.get("LiveParametersV2", return_default=True) is None
|
||||
|
||||
|
||||
@@ -11,13 +11,13 @@ from opendbc.car.structs import car
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import config_realtime_process, Priority, Ratekeeper
|
||||
from openpilot.common.swaglog import cloudlog, ForwardingHandler
|
||||
|
||||
from opendbc.car import DT_CTRL, structs
|
||||
from opendbc.car.can_definitions import CanData, CanRecvCallable, CanSendCallable
|
||||
from opendbc.car.carlog import carlog
|
||||
from opendbc.car.fw_versions import ObdCallback
|
||||
from opendbc.car.car_helpers import get_car, interfaces
|
||||
from opendbc.car.interfaces import CarInterfaceBase, RadarInterfaceBase
|
||||
from opendbc.safety import ALTERNATIVE_EXPERIENCE
|
||||
from openpilot.selfdrive.pandad import can_capnp_to_list, can_list_to_can_capnp
|
||||
from openpilot.selfdrive.car.cruise import VCruiseHelper
|
||||
from openpilot.selfdrive.car.helpers import convert_carControlSP, convert_to_capnp
|
||||
@@ -123,6 +123,9 @@ class Car:
|
||||
self.RI = RI
|
||||
|
||||
self.CP.alternativeExperience = 0
|
||||
if self.params.get_bool("ToyotaAutoHold"):
|
||||
self.CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.ALLOW_AEB
|
||||
|
||||
# mads
|
||||
set_alternative_experience(self.CP, self.CP_SP, self.params)
|
||||
set_car_specific_params(self.CP, self.CP_SP, self.params)
|
||||
|
||||
@@ -19,6 +19,7 @@ IMPERIAL_INCREMENT = round(CV.MPH_TO_KPH, 1) # round here to avoid rounding err
|
||||
ButtonEvent = car.CarState.ButtonEvent
|
||||
ButtonType = car.CarState.ButtonEvent.Type
|
||||
CRUISE_LONG_PRESS = 50
|
||||
TOYOTA_VIRTUAL_CRUISE_LONG_PRESS = 65
|
||||
CRUISE_NEAREST_FUNC = {
|
||||
ButtonType.accelCruise: math.ceil,
|
||||
ButtonType.decelCruise: math.floor,
|
||||
@@ -43,6 +44,30 @@ class VCruiseHelper(VCruiseHelperSP):
|
||||
def v_cruise_initialized(self):
|
||||
return self.v_cruise_kph != V_CRUISE_UNSET
|
||||
|
||||
@property
|
||||
def software_pcm_cruise_speed(self) -> bool:
|
||||
return self.CP.brand == "toyota" and self.CP.pcmCruise and self.CP.openpilotLongitudinalControl and not self.CP_SP.pcmCruiseSpeed
|
||||
|
||||
@property
|
||||
def cruise_long_press_frames(self) -> int:
|
||||
return TOYOTA_VIRTUAL_CRUISE_LONG_PRESS if self.software_pcm_cruise_speed else CRUISE_LONG_PRESS
|
||||
|
||||
@property
|
||||
def software_pcm_cruise_initialized(self) -> bool:
|
||||
return 0 < self.v_cruise_kph < V_CRUISE_UNSET and 0 < self.v_cruise_cluster_kph < V_CRUISE_UNSET
|
||||
|
||||
def _apply_software_pcm_cruise_delta(self, delta_kph: float, is_metric: bool) -> None:
|
||||
"""Move Toyota's planner/display targets together while respecting both targets' bounds."""
|
||||
cluster_min_kph = self.v_cruise_min if is_metric else self.v_cruise_min * CV.MPH_TO_KPH
|
||||
min_delta = max(V_CRUISE_MIN - self.v_cruise_kph, cluster_min_kph - self.v_cruise_cluster_kph)
|
||||
max_delta = min(V_CRUISE_MAX - self.v_cruise_kph, V_CRUISE_MAX - self.v_cruise_cluster_kph)
|
||||
if delta_kph > 0:
|
||||
applied_delta = min(delta_kph, max(0., max_delta))
|
||||
else:
|
||||
applied_delta = max(delta_kph, min(0., min_delta))
|
||||
self.v_cruise_kph = round(self.v_cruise_kph + applied_delta, 1)
|
||||
self.v_cruise_cluster_kph = round(self.v_cruise_cluster_kph + applied_delta, 1)
|
||||
|
||||
def update_v_cruise(self, CS, enabled, is_metric):
|
||||
self.v_cruise_kph_last = self.v_cruise_kph
|
||||
|
||||
@@ -51,11 +76,21 @@ class VCruiseHelper(VCruiseHelperSP):
|
||||
_enabled = self.update_enabled_state(CS, enabled)
|
||||
|
||||
if CS.cruiseState.available:
|
||||
if not self.CP.pcmCruise or (not self.CP_SP.pcmCruiseSpeed and _enabled):
|
||||
software_pcm_enabled = not self.CP_SP.pcmCruiseSpeed and _enabled
|
||||
if self.software_pcm_cruise_speed:
|
||||
software_pcm_enabled = software_pcm_enabled and self.software_pcm_cruise_initialized
|
||||
|
||||
if not self.CP.pcmCruise or software_pcm_enabled:
|
||||
# if stock cruise is completely disabled, then we can use our own set speed logic
|
||||
self._update_v_cruise_non_pcm(CS, _enabled, is_metric)
|
||||
v_cruise_kph_before_sla = self.v_cruise_kph
|
||||
self.update_speed_limit_assist_v_cruise_non_pcm()
|
||||
self.v_cruise_cluster_kph = self.v_cruise_kph
|
||||
if self.software_pcm_cruise_speed:
|
||||
sla_delta_kph = self.v_cruise_kph - v_cruise_kph_before_sla
|
||||
self.v_cruise_kph = v_cruise_kph_before_sla
|
||||
self._apply_software_pcm_cruise_delta(sla_delta_kph, is_metric)
|
||||
else:
|
||||
self.v_cruise_cluster_kph = self.v_cruise_kph
|
||||
else:
|
||||
self.v_cruise_kph = CS.cruiseState.speed * CV.MS_TO_KPH
|
||||
self.v_cruise_cluster_kph = CS.cruiseState.speedCluster * CV.MS_TO_KPH
|
||||
@@ -85,13 +120,13 @@ class VCruiseHelper(VCruiseHelperSP):
|
||||
|
||||
for b in CS.buttonEvents:
|
||||
if b.type.raw in self.button_timers and not b.pressed:
|
||||
if self.button_timers[b.type.raw] > CRUISE_LONG_PRESS:
|
||||
if self.button_timers[b.type.raw] > self.cruise_long_press_frames:
|
||||
return # end long press
|
||||
button_type = b.type.raw
|
||||
break
|
||||
else:
|
||||
for k, timer in self.button_timers.items():
|
||||
if timer and timer % CRUISE_LONG_PRESS == 0:
|
||||
if timer and timer % self.cruise_long_press_frames == 0:
|
||||
button_type = k
|
||||
long_press = True
|
||||
break
|
||||
@@ -115,10 +150,26 @@ class VCruiseHelper(VCruiseHelperSP):
|
||||
return
|
||||
|
||||
long_press, v_cruise_delta = VCruiseHelperSP.update_v_cruise_delta(self, long_press, v_cruise_delta)
|
||||
if long_press and self.v_cruise_kph % v_cruise_delta != 0: # partial interval
|
||||
self.v_cruise_kph = CRUISE_NEAREST_FUNC[button_type](self.v_cruise_kph / v_cruise_delta) * v_cruise_delta
|
||||
# Toyota's canonical PCM set speed and displayed cluster set speed can differ. In
|
||||
# software-owned PCM mode, round the value the driver sees and apply the same delta
|
||||
# to both targets so the planner/cluster calibration offset remains intact.
|
||||
v_cruise_reference = self.v_cruise_cluster_kph if self.software_pcm_cruise_speed else self.v_cruise_kph
|
||||
if long_press and v_cruise_reference % v_cruise_delta != 0: # partial interval
|
||||
v_cruise_reference_new = CRUISE_NEAREST_FUNC[button_type](v_cruise_reference / v_cruise_delta) * v_cruise_delta
|
||||
else:
|
||||
self.v_cruise_kph += v_cruise_delta * CRUISE_INTERVAL_SIGN[button_type]
|
||||
v_cruise_reference_new = v_cruise_reference + v_cruise_delta * CRUISE_INTERVAL_SIGN[button_type]
|
||||
|
||||
if self.software_pcm_cruise_speed:
|
||||
delta_kph = v_cruise_reference_new - v_cruise_reference
|
||||
|
||||
# If SET is pressed while overriding, do not lower the target below the current speed.
|
||||
if CS.gasPressed and button_type in (ButtonType.decelCruise, ButtonType.setCruise):
|
||||
delta_kph = max(delta_kph, CS.vEgo * CV.MS_TO_KPH - self.v_cruise_kph)
|
||||
|
||||
self._apply_software_pcm_cruise_delta(delta_kph, is_metric)
|
||||
return
|
||||
|
||||
self.v_cruise_kph += v_cruise_reference_new - v_cruise_reference
|
||||
|
||||
# If set is pressed while overriding, clip cruise speed to minimum of vEgo
|
||||
if CS.gasPressed and button_type in (ButtonType.decelCruise, ButtonType.setCruise):
|
||||
@@ -127,6 +178,12 @@ class VCruiseHelper(VCruiseHelperSP):
|
||||
self.v_cruise_kph = np.clip(round(self.v_cruise_kph, 1), self.v_cruise_min, V_CRUISE_MAX)
|
||||
|
||||
def update_button_timers(self, CS, enabled):
|
||||
if self.software_pcm_cruise_speed and (not enabled or not CS.cruiseState.available or not self.software_pcm_cruise_initialized):
|
||||
for k in self.button_timers:
|
||||
self.button_timers[k] = 0
|
||||
self.button_change_states[k] = {"standstill": False, "enabled": False}
|
||||
return
|
||||
|
||||
# increment timer for buttons still pressed
|
||||
for k in self.button_timers:
|
||||
if self.button_timers[k] > 0:
|
||||
|
||||
@@ -136,6 +136,7 @@ class Controls(ControlsExt):
|
||||
# accel PID loop
|
||||
pid_accel_limits = self.CI.get_pid_accel_limits(self.CP, self.CP_SP, CS.vEgo, CS.vCruise * CV.KPH_TO_MS)
|
||||
actuators.accel = float(self.LoC.update(CC.longActive, CS, long_plan.aTarget, long_plan.shouldStop, pid_accel_limits))
|
||||
self.long_accel_sp(actuators, CS, long_plan, pid_accel_limits) # sunnypilot hook
|
||||
|
||||
# Steering PID loop and lateral MPC
|
||||
# Reset desired curvature to current to avoid violating the limits on engage
|
||||
|
||||
@@ -4,7 +4,6 @@ from openpilot.common.realtime import DT_CTRL
|
||||
from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N
|
||||
from openpilot.common.pid import PIDController
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.longcontrol import LongControlSP
|
||||
|
||||
CONTROL_N_T_IDX = ModelConstants.T_IDXS[:CONTROL_N]
|
||||
|
||||
@@ -40,8 +39,7 @@ def long_control_state_trans(CP_SP, active, long_control_state,
|
||||
|
||||
return long_control_state
|
||||
|
||||
|
||||
class LongControl(LongControlSP):
|
||||
class LongControl:
|
||||
def __init__(self, CP, CP_SP):
|
||||
self.CP = CP
|
||||
self.CP_SP = CP_SP
|
||||
@@ -70,7 +68,7 @@ class LongControl(LongControlSP):
|
||||
if output_accel > self.CP.stopAccel:
|
||||
output_accel = min(output_accel, 0.0)
|
||||
# TODO: can we just go straight to stopAccel?
|
||||
output_accel -= self.stopping_decel_rate(CS.vEgo) * DT_CTRL
|
||||
output_accel -= 1.0 * DT_CTRL # m/s^2/s while trying to stop
|
||||
self.reset()
|
||||
|
||||
else: # LongCtrlState.pid
|
||||
|
||||
@@ -35,9 +35,12 @@ def get_max_accel(v_ego):
|
||||
def get_coast_accel(pitch):
|
||||
return np.sin(pitch) * -5.65 - 0.3 # fitted from data using xx/projects/allow_throttle/compute_coast_accel.py
|
||||
|
||||
def get_cruise_accel(e2e, v_cruise, v_ego, a_cruise_prev, angle_steers, CP, dt, accel_coast, allow_throttle):
|
||||
max_accel = ACCEL_MAX if e2e else get_max_accel(v_ego)
|
||||
|
||||
def get_cruise_accel(e2e, v_cruise, v_ego, a_cruise_prev, angle_steers, CP, dt, accel_coast, allow_throttle,
|
||||
max_accel_override=None):
|
||||
if max_accel_override is not None:
|
||||
max_accel = max_accel_override
|
||||
else:
|
||||
max_accel = ACCEL_MAX if e2e else get_max_accel(v_ego)
|
||||
if not e2e:
|
||||
a_total_max = np.interp(v_ego, _A_TOTAL_MAX_BP, _A_TOTAL_MAX_V)
|
||||
a_y = v_ego ** 2 * angle_steers * CV.DEG_TO_RAD / (CP.steerRatio * CP.wheelbase)
|
||||
@@ -84,7 +87,8 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
v_ego = sm['carState'].vEgo
|
||||
v_cruise_kph = min(sm['carState'].vCruise, V_CRUISE_MAX)
|
||||
v_cruise = v_cruise_kph * CV.KPH_TO_MS
|
||||
if sm['controlsState'].forceDecel:
|
||||
force_decel = sm['controlsState'].forceDecel
|
||||
if force_decel:
|
||||
v_cruise = 0.0
|
||||
|
||||
long_control_off = sm['controlsState'].longControlState == LongCtrlState.off
|
||||
@@ -118,6 +122,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
self.mpc.set_weights(prev_accel_constraint, personality=sm['selfdriveState'].personality)
|
||||
self.mpc.set_cur_state(self.v_desired_filter.x, self.output_a_target)
|
||||
self.mpc.update(sm['radarState'], personality=sm['selfdriveState'].personality)
|
||||
self.update_dec(sm)
|
||||
|
||||
self.v_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.v_solution)
|
||||
self.a_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.a_solution)
|
||||
@@ -140,9 +145,17 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
|
||||
is_e2e = self.is_e2e(sm)
|
||||
|
||||
self.a_cruise = get_cruise_accel(is_e2e, v_cruise, v_ego,
|
||||
self.a_cruise, steer_angle_without_offset, self.CP, self.dt,
|
||||
accel_coast, self.allow_throttle)
|
||||
max_accel_override = self.get_max_accel_override(v_ego, sm['carStateSP'].engineOff)
|
||||
v_cruise = self.get_cruise_target_override(v_ego, v_cruise, force_decel, accel_coast if accel_coast < 0.0 else None)
|
||||
a_cruise_prev = self.a_cruise
|
||||
gated_cruise = get_cruise_accel(is_e2e, v_cruise, v_ego, a_cruise_prev, steer_angle_without_offset,
|
||||
self.CP, self.dt, accel_coast, self.allow_throttle, max_accel_override)
|
||||
ungated_cruise = get_cruise_accel(is_e2e, v_cruise, v_ego, a_cruise_prev, steer_angle_without_offset,
|
||||
self.CP, self.dt, accel_coast, True, max_accel_override)
|
||||
self.a_cruise = self.arbitrate_cruise_candidate(
|
||||
sm, gated_cruise, ungated_cruise, output_a_target_mpc, self.mpc.source,
|
||||
allow_throttle=self.allow_throttle, e2e=is_e2e, force_decel=force_decel,
|
||||
)
|
||||
cruise_should_stop = should_stop(v_ego, self.a_cruise)
|
||||
|
||||
candidates = [(output_a_target_mpc, self.mpc.source, output_should_stop_mpc),
|
||||
@@ -153,6 +166,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
output_a_target, self.mpc.source, _ = min(candidates, key=lambda c: c[0])
|
||||
self.output_should_stop = any(should_stop for _, _, should_stop in candidates)
|
||||
self.output_a_target = np.clip(output_a_target, ACCEL_MIN, ACCEL_MAX)
|
||||
self.accel_controller_active = self.is_accel_controller_active(force_decel)
|
||||
|
||||
self.v_desired_filter.x = self.v_desired_filter.x + self.dt * (self.output_a_target + a_prev) / 2.0
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
from types import SimpleNamespace
|
||||
from typing import cast
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.cereal import custom
|
||||
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState, long_control_state_trans
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.longcontrol import LongControlSP
|
||||
|
||||
|
||||
class TestLongControlStateTransition(OpenpilotTestCase):
|
||||
@@ -46,21 +42,3 @@ class TestLongControlStateTransition(OpenpilotTestCase):
|
||||
next_state = long_control_state_trans(CP_SP, active, current_state,
|
||||
should_stop=False, brake_pressed=False, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.pid
|
||||
|
||||
|
||||
class TestStoppingHold:
|
||||
def test_weak_brake_does_not_hold_stopping(self):
|
||||
control = SimpleNamespace(last_output_accel=-0.109)
|
||||
car_state = SimpleNamespace(vEgo=0.277, aEgo=-0.406)
|
||||
assert not LongControlSP.should_hold_stopping(cast(LongControlSP, control), car_state, -0.072)
|
||||
|
||||
def test_sufficient_brake_holds_stopping(self):
|
||||
control = SimpleNamespace(last_output_accel=-0.543)
|
||||
car_state = SimpleNamespace(vEgo=0.209, aEgo=-0.534)
|
||||
assert LongControlSP.should_hold_stopping(cast(LongControlSP, control), car_state, -0.469)
|
||||
|
||||
def test_stopping_decel_rate_is_smooth_while_rolling(self):
|
||||
assert LongControlSP.stopping_decel_rate(0.1) == 0.3
|
||||
|
||||
def test_stopping_decel_rate_builds_holding_brake_at_stop(self):
|
||||
assert LongControlSP.stopping_decel_rate(0.0) == 2.0
|
||||
|
||||
@@ -45,326 +45,326 @@ const static double MAHA_THRESH_31 = 3.8414588206941227;
|
||||
* *
|
||||
* This file is part of 'ekf' *
|
||||
******************************************************************************/
|
||||
void err_fun(double *nom_x, double *delta_x, double *out_205475353292099530) {
|
||||
out_205475353292099530[0] = delta_x[0] + nom_x[0];
|
||||
out_205475353292099530[1] = delta_x[1] + nom_x[1];
|
||||
out_205475353292099530[2] = delta_x[2] + nom_x[2];
|
||||
out_205475353292099530[3] = delta_x[3] + nom_x[3];
|
||||
out_205475353292099530[4] = delta_x[4] + nom_x[4];
|
||||
out_205475353292099530[5] = delta_x[5] + nom_x[5];
|
||||
out_205475353292099530[6] = delta_x[6] + nom_x[6];
|
||||
out_205475353292099530[7] = delta_x[7] + nom_x[7];
|
||||
out_205475353292099530[8] = delta_x[8] + nom_x[8];
|
||||
void err_fun(double *nom_x, double *delta_x, double *out_5070392900769873758) {
|
||||
out_5070392900769873758[0] = delta_x[0] + nom_x[0];
|
||||
out_5070392900769873758[1] = delta_x[1] + nom_x[1];
|
||||
out_5070392900769873758[2] = delta_x[2] + nom_x[2];
|
||||
out_5070392900769873758[3] = delta_x[3] + nom_x[3];
|
||||
out_5070392900769873758[4] = delta_x[4] + nom_x[4];
|
||||
out_5070392900769873758[5] = delta_x[5] + nom_x[5];
|
||||
out_5070392900769873758[6] = delta_x[6] + nom_x[6];
|
||||
out_5070392900769873758[7] = delta_x[7] + nom_x[7];
|
||||
out_5070392900769873758[8] = delta_x[8] + nom_x[8];
|
||||
}
|
||||
void inv_err_fun(double *nom_x, double *true_x, double *out_6859088956493527341) {
|
||||
out_6859088956493527341[0] = -nom_x[0] + true_x[0];
|
||||
out_6859088956493527341[1] = -nom_x[1] + true_x[1];
|
||||
out_6859088956493527341[2] = -nom_x[2] + true_x[2];
|
||||
out_6859088956493527341[3] = -nom_x[3] + true_x[3];
|
||||
out_6859088956493527341[4] = -nom_x[4] + true_x[4];
|
||||
out_6859088956493527341[5] = -nom_x[5] + true_x[5];
|
||||
out_6859088956493527341[6] = -nom_x[6] + true_x[6];
|
||||
out_6859088956493527341[7] = -nom_x[7] + true_x[7];
|
||||
out_6859088956493527341[8] = -nom_x[8] + true_x[8];
|
||||
void inv_err_fun(double *nom_x, double *true_x, double *out_3742760733154016051) {
|
||||
out_3742760733154016051[0] = -nom_x[0] + true_x[0];
|
||||
out_3742760733154016051[1] = -nom_x[1] + true_x[1];
|
||||
out_3742760733154016051[2] = -nom_x[2] + true_x[2];
|
||||
out_3742760733154016051[3] = -nom_x[3] + true_x[3];
|
||||
out_3742760733154016051[4] = -nom_x[4] + true_x[4];
|
||||
out_3742760733154016051[5] = -nom_x[5] + true_x[5];
|
||||
out_3742760733154016051[6] = -nom_x[6] + true_x[6];
|
||||
out_3742760733154016051[7] = -nom_x[7] + true_x[7];
|
||||
out_3742760733154016051[8] = -nom_x[8] + true_x[8];
|
||||
}
|
||||
void H_mod_fun(double *state, double *out_8201686483145055601) {
|
||||
out_8201686483145055601[0] = 1.0;
|
||||
out_8201686483145055601[1] = 0.0;
|
||||
out_8201686483145055601[2] = 0.0;
|
||||
out_8201686483145055601[3] = 0.0;
|
||||
out_8201686483145055601[4] = 0.0;
|
||||
out_8201686483145055601[5] = 0.0;
|
||||
out_8201686483145055601[6] = 0.0;
|
||||
out_8201686483145055601[7] = 0.0;
|
||||
out_8201686483145055601[8] = 0.0;
|
||||
out_8201686483145055601[9] = 0.0;
|
||||
out_8201686483145055601[10] = 1.0;
|
||||
out_8201686483145055601[11] = 0.0;
|
||||
out_8201686483145055601[12] = 0.0;
|
||||
out_8201686483145055601[13] = 0.0;
|
||||
out_8201686483145055601[14] = 0.0;
|
||||
out_8201686483145055601[15] = 0.0;
|
||||
out_8201686483145055601[16] = 0.0;
|
||||
out_8201686483145055601[17] = 0.0;
|
||||
out_8201686483145055601[18] = 0.0;
|
||||
out_8201686483145055601[19] = 0.0;
|
||||
out_8201686483145055601[20] = 1.0;
|
||||
out_8201686483145055601[21] = 0.0;
|
||||
out_8201686483145055601[22] = 0.0;
|
||||
out_8201686483145055601[23] = 0.0;
|
||||
out_8201686483145055601[24] = 0.0;
|
||||
out_8201686483145055601[25] = 0.0;
|
||||
out_8201686483145055601[26] = 0.0;
|
||||
out_8201686483145055601[27] = 0.0;
|
||||
out_8201686483145055601[28] = 0.0;
|
||||
out_8201686483145055601[29] = 0.0;
|
||||
out_8201686483145055601[30] = 1.0;
|
||||
out_8201686483145055601[31] = 0.0;
|
||||
out_8201686483145055601[32] = 0.0;
|
||||
out_8201686483145055601[33] = 0.0;
|
||||
out_8201686483145055601[34] = 0.0;
|
||||
out_8201686483145055601[35] = 0.0;
|
||||
out_8201686483145055601[36] = 0.0;
|
||||
out_8201686483145055601[37] = 0.0;
|
||||
out_8201686483145055601[38] = 0.0;
|
||||
out_8201686483145055601[39] = 0.0;
|
||||
out_8201686483145055601[40] = 1.0;
|
||||
out_8201686483145055601[41] = 0.0;
|
||||
out_8201686483145055601[42] = 0.0;
|
||||
out_8201686483145055601[43] = 0.0;
|
||||
out_8201686483145055601[44] = 0.0;
|
||||
out_8201686483145055601[45] = 0.0;
|
||||
out_8201686483145055601[46] = 0.0;
|
||||
out_8201686483145055601[47] = 0.0;
|
||||
out_8201686483145055601[48] = 0.0;
|
||||
out_8201686483145055601[49] = 0.0;
|
||||
out_8201686483145055601[50] = 1.0;
|
||||
out_8201686483145055601[51] = 0.0;
|
||||
out_8201686483145055601[52] = 0.0;
|
||||
out_8201686483145055601[53] = 0.0;
|
||||
out_8201686483145055601[54] = 0.0;
|
||||
out_8201686483145055601[55] = 0.0;
|
||||
out_8201686483145055601[56] = 0.0;
|
||||
out_8201686483145055601[57] = 0.0;
|
||||
out_8201686483145055601[58] = 0.0;
|
||||
out_8201686483145055601[59] = 0.0;
|
||||
out_8201686483145055601[60] = 1.0;
|
||||
out_8201686483145055601[61] = 0.0;
|
||||
out_8201686483145055601[62] = 0.0;
|
||||
out_8201686483145055601[63] = 0.0;
|
||||
out_8201686483145055601[64] = 0.0;
|
||||
out_8201686483145055601[65] = 0.0;
|
||||
out_8201686483145055601[66] = 0.0;
|
||||
out_8201686483145055601[67] = 0.0;
|
||||
out_8201686483145055601[68] = 0.0;
|
||||
out_8201686483145055601[69] = 0.0;
|
||||
out_8201686483145055601[70] = 1.0;
|
||||
out_8201686483145055601[71] = 0.0;
|
||||
out_8201686483145055601[72] = 0.0;
|
||||
out_8201686483145055601[73] = 0.0;
|
||||
out_8201686483145055601[74] = 0.0;
|
||||
out_8201686483145055601[75] = 0.0;
|
||||
out_8201686483145055601[76] = 0.0;
|
||||
out_8201686483145055601[77] = 0.0;
|
||||
out_8201686483145055601[78] = 0.0;
|
||||
out_8201686483145055601[79] = 0.0;
|
||||
out_8201686483145055601[80] = 1.0;
|
||||
void H_mod_fun(double *state, double *out_5758456641177717920) {
|
||||
out_5758456641177717920[0] = 1.0;
|
||||
out_5758456641177717920[1] = 0.0;
|
||||
out_5758456641177717920[2] = 0.0;
|
||||
out_5758456641177717920[3] = 0.0;
|
||||
out_5758456641177717920[4] = 0.0;
|
||||
out_5758456641177717920[5] = 0.0;
|
||||
out_5758456641177717920[6] = 0.0;
|
||||
out_5758456641177717920[7] = 0.0;
|
||||
out_5758456641177717920[8] = 0.0;
|
||||
out_5758456641177717920[9] = 0.0;
|
||||
out_5758456641177717920[10] = 1.0;
|
||||
out_5758456641177717920[11] = 0.0;
|
||||
out_5758456641177717920[12] = 0.0;
|
||||
out_5758456641177717920[13] = 0.0;
|
||||
out_5758456641177717920[14] = 0.0;
|
||||
out_5758456641177717920[15] = 0.0;
|
||||
out_5758456641177717920[16] = 0.0;
|
||||
out_5758456641177717920[17] = 0.0;
|
||||
out_5758456641177717920[18] = 0.0;
|
||||
out_5758456641177717920[19] = 0.0;
|
||||
out_5758456641177717920[20] = 1.0;
|
||||
out_5758456641177717920[21] = 0.0;
|
||||
out_5758456641177717920[22] = 0.0;
|
||||
out_5758456641177717920[23] = 0.0;
|
||||
out_5758456641177717920[24] = 0.0;
|
||||
out_5758456641177717920[25] = 0.0;
|
||||
out_5758456641177717920[26] = 0.0;
|
||||
out_5758456641177717920[27] = 0.0;
|
||||
out_5758456641177717920[28] = 0.0;
|
||||
out_5758456641177717920[29] = 0.0;
|
||||
out_5758456641177717920[30] = 1.0;
|
||||
out_5758456641177717920[31] = 0.0;
|
||||
out_5758456641177717920[32] = 0.0;
|
||||
out_5758456641177717920[33] = 0.0;
|
||||
out_5758456641177717920[34] = 0.0;
|
||||
out_5758456641177717920[35] = 0.0;
|
||||
out_5758456641177717920[36] = 0.0;
|
||||
out_5758456641177717920[37] = 0.0;
|
||||
out_5758456641177717920[38] = 0.0;
|
||||
out_5758456641177717920[39] = 0.0;
|
||||
out_5758456641177717920[40] = 1.0;
|
||||
out_5758456641177717920[41] = 0.0;
|
||||
out_5758456641177717920[42] = 0.0;
|
||||
out_5758456641177717920[43] = 0.0;
|
||||
out_5758456641177717920[44] = 0.0;
|
||||
out_5758456641177717920[45] = 0.0;
|
||||
out_5758456641177717920[46] = 0.0;
|
||||
out_5758456641177717920[47] = 0.0;
|
||||
out_5758456641177717920[48] = 0.0;
|
||||
out_5758456641177717920[49] = 0.0;
|
||||
out_5758456641177717920[50] = 1.0;
|
||||
out_5758456641177717920[51] = 0.0;
|
||||
out_5758456641177717920[52] = 0.0;
|
||||
out_5758456641177717920[53] = 0.0;
|
||||
out_5758456641177717920[54] = 0.0;
|
||||
out_5758456641177717920[55] = 0.0;
|
||||
out_5758456641177717920[56] = 0.0;
|
||||
out_5758456641177717920[57] = 0.0;
|
||||
out_5758456641177717920[58] = 0.0;
|
||||
out_5758456641177717920[59] = 0.0;
|
||||
out_5758456641177717920[60] = 1.0;
|
||||
out_5758456641177717920[61] = 0.0;
|
||||
out_5758456641177717920[62] = 0.0;
|
||||
out_5758456641177717920[63] = 0.0;
|
||||
out_5758456641177717920[64] = 0.0;
|
||||
out_5758456641177717920[65] = 0.0;
|
||||
out_5758456641177717920[66] = 0.0;
|
||||
out_5758456641177717920[67] = 0.0;
|
||||
out_5758456641177717920[68] = 0.0;
|
||||
out_5758456641177717920[69] = 0.0;
|
||||
out_5758456641177717920[70] = 1.0;
|
||||
out_5758456641177717920[71] = 0.0;
|
||||
out_5758456641177717920[72] = 0.0;
|
||||
out_5758456641177717920[73] = 0.0;
|
||||
out_5758456641177717920[74] = 0.0;
|
||||
out_5758456641177717920[75] = 0.0;
|
||||
out_5758456641177717920[76] = 0.0;
|
||||
out_5758456641177717920[77] = 0.0;
|
||||
out_5758456641177717920[78] = 0.0;
|
||||
out_5758456641177717920[79] = 0.0;
|
||||
out_5758456641177717920[80] = 1.0;
|
||||
}
|
||||
void f_fun(double *state, double dt, double *out_2204957494938772746) {
|
||||
out_2204957494938772746[0] = state[0];
|
||||
out_2204957494938772746[1] = state[1];
|
||||
out_2204957494938772746[2] = state[2];
|
||||
out_2204957494938772746[3] = state[3];
|
||||
out_2204957494938772746[4] = state[4];
|
||||
out_2204957494938772746[5] = dt*((-state[4] + (-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])/(mass*state[4]))*state[6] - 9.8100000000000005*state[8] + stiffness_front*(-state[2] - state[3] + state[7])*state[0]/(mass*state[1]) + (-stiffness_front*state[0] - stiffness_rear*state[0])*state[5]/(mass*state[4])) + state[5];
|
||||
out_2204957494938772746[6] = dt*(center_to_front*stiffness_front*(-state[2] - state[3] + state[7])*state[0]/(rotational_inertia*state[1]) + (-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])*state[5]/(rotational_inertia*state[4]) + (-pow(center_to_front, 2)*stiffness_front*state[0] - pow(center_to_rear, 2)*stiffness_rear*state[0])*state[6]/(rotational_inertia*state[4])) + state[6];
|
||||
out_2204957494938772746[7] = state[7];
|
||||
out_2204957494938772746[8] = state[8];
|
||||
void f_fun(double *state, double dt, double *out_730595102125265078) {
|
||||
out_730595102125265078[0] = state[0];
|
||||
out_730595102125265078[1] = state[1];
|
||||
out_730595102125265078[2] = state[2];
|
||||
out_730595102125265078[3] = state[3];
|
||||
out_730595102125265078[4] = state[4];
|
||||
out_730595102125265078[5] = dt*((-state[4] + (-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])/(mass*state[4]))*state[6] - 9.8100000000000005*state[8] + stiffness_front*(-state[2] - state[3] + state[7])*state[0]/(mass*state[1]) + (-stiffness_front*state[0] - stiffness_rear*state[0])*state[5]/(mass*state[4])) + state[5];
|
||||
out_730595102125265078[6] = dt*(center_to_front*stiffness_front*(-state[2] - state[3] + state[7])*state[0]/(rotational_inertia*state[1]) + (-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])*state[5]/(rotational_inertia*state[4]) + (-pow(center_to_front, 2)*stiffness_front*state[0] - pow(center_to_rear, 2)*stiffness_rear*state[0])*state[6]/(rotational_inertia*state[4])) + state[6];
|
||||
out_730595102125265078[7] = state[7];
|
||||
out_730595102125265078[8] = state[8];
|
||||
}
|
||||
void F_fun(double *state, double dt, double *out_2327939745960551937) {
|
||||
out_2327939745960551937[0] = 1;
|
||||
out_2327939745960551937[1] = 0;
|
||||
out_2327939745960551937[2] = 0;
|
||||
out_2327939745960551937[3] = 0;
|
||||
out_2327939745960551937[4] = 0;
|
||||
out_2327939745960551937[5] = 0;
|
||||
out_2327939745960551937[6] = 0;
|
||||
out_2327939745960551937[7] = 0;
|
||||
out_2327939745960551937[8] = 0;
|
||||
out_2327939745960551937[9] = 0;
|
||||
out_2327939745960551937[10] = 1;
|
||||
out_2327939745960551937[11] = 0;
|
||||
out_2327939745960551937[12] = 0;
|
||||
out_2327939745960551937[13] = 0;
|
||||
out_2327939745960551937[14] = 0;
|
||||
out_2327939745960551937[15] = 0;
|
||||
out_2327939745960551937[16] = 0;
|
||||
out_2327939745960551937[17] = 0;
|
||||
out_2327939745960551937[18] = 0;
|
||||
out_2327939745960551937[19] = 0;
|
||||
out_2327939745960551937[20] = 1;
|
||||
out_2327939745960551937[21] = 0;
|
||||
out_2327939745960551937[22] = 0;
|
||||
out_2327939745960551937[23] = 0;
|
||||
out_2327939745960551937[24] = 0;
|
||||
out_2327939745960551937[25] = 0;
|
||||
out_2327939745960551937[26] = 0;
|
||||
out_2327939745960551937[27] = 0;
|
||||
out_2327939745960551937[28] = 0;
|
||||
out_2327939745960551937[29] = 0;
|
||||
out_2327939745960551937[30] = 1;
|
||||
out_2327939745960551937[31] = 0;
|
||||
out_2327939745960551937[32] = 0;
|
||||
out_2327939745960551937[33] = 0;
|
||||
out_2327939745960551937[34] = 0;
|
||||
out_2327939745960551937[35] = 0;
|
||||
out_2327939745960551937[36] = 0;
|
||||
out_2327939745960551937[37] = 0;
|
||||
out_2327939745960551937[38] = 0;
|
||||
out_2327939745960551937[39] = 0;
|
||||
out_2327939745960551937[40] = 1;
|
||||
out_2327939745960551937[41] = 0;
|
||||
out_2327939745960551937[42] = 0;
|
||||
out_2327939745960551937[43] = 0;
|
||||
out_2327939745960551937[44] = 0;
|
||||
out_2327939745960551937[45] = dt*(stiffness_front*(-state[2] - state[3] + state[7])/(mass*state[1]) + (-stiffness_front - stiffness_rear)*state[5]/(mass*state[4]) + (-center_to_front*stiffness_front + center_to_rear*stiffness_rear)*state[6]/(mass*state[4]));
|
||||
out_2327939745960551937[46] = -dt*stiffness_front*(-state[2] - state[3] + state[7])*state[0]/(mass*pow(state[1], 2));
|
||||
out_2327939745960551937[47] = -dt*stiffness_front*state[0]/(mass*state[1]);
|
||||
out_2327939745960551937[48] = -dt*stiffness_front*state[0]/(mass*state[1]);
|
||||
out_2327939745960551937[49] = dt*((-1 - (-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])/(mass*pow(state[4], 2)))*state[6] - (-stiffness_front*state[0] - stiffness_rear*state[0])*state[5]/(mass*pow(state[4], 2)));
|
||||
out_2327939745960551937[50] = dt*(-stiffness_front*state[0] - stiffness_rear*state[0])/(mass*state[4]) + 1;
|
||||
out_2327939745960551937[51] = dt*(-state[4] + (-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])/(mass*state[4]));
|
||||
out_2327939745960551937[52] = dt*stiffness_front*state[0]/(mass*state[1]);
|
||||
out_2327939745960551937[53] = -9.8100000000000005*dt;
|
||||
out_2327939745960551937[54] = dt*(center_to_front*stiffness_front*(-state[2] - state[3] + state[7])/(rotational_inertia*state[1]) + (-center_to_front*stiffness_front + center_to_rear*stiffness_rear)*state[5]/(rotational_inertia*state[4]) + (-pow(center_to_front, 2)*stiffness_front - pow(center_to_rear, 2)*stiffness_rear)*state[6]/(rotational_inertia*state[4]));
|
||||
out_2327939745960551937[55] = -center_to_front*dt*stiffness_front*(-state[2] - state[3] + state[7])*state[0]/(rotational_inertia*pow(state[1], 2));
|
||||
out_2327939745960551937[56] = -center_to_front*dt*stiffness_front*state[0]/(rotational_inertia*state[1]);
|
||||
out_2327939745960551937[57] = -center_to_front*dt*stiffness_front*state[0]/(rotational_inertia*state[1]);
|
||||
out_2327939745960551937[58] = dt*(-(-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])*state[5]/(rotational_inertia*pow(state[4], 2)) - (-pow(center_to_front, 2)*stiffness_front*state[0] - pow(center_to_rear, 2)*stiffness_rear*state[0])*state[6]/(rotational_inertia*pow(state[4], 2)));
|
||||
out_2327939745960551937[59] = dt*(-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])/(rotational_inertia*state[4]);
|
||||
out_2327939745960551937[60] = dt*(-pow(center_to_front, 2)*stiffness_front*state[0] - pow(center_to_rear, 2)*stiffness_rear*state[0])/(rotational_inertia*state[4]) + 1;
|
||||
out_2327939745960551937[61] = center_to_front*dt*stiffness_front*state[0]/(rotational_inertia*state[1]);
|
||||
out_2327939745960551937[62] = 0;
|
||||
out_2327939745960551937[63] = 0;
|
||||
out_2327939745960551937[64] = 0;
|
||||
out_2327939745960551937[65] = 0;
|
||||
out_2327939745960551937[66] = 0;
|
||||
out_2327939745960551937[67] = 0;
|
||||
out_2327939745960551937[68] = 0;
|
||||
out_2327939745960551937[69] = 0;
|
||||
out_2327939745960551937[70] = 1;
|
||||
out_2327939745960551937[71] = 0;
|
||||
out_2327939745960551937[72] = 0;
|
||||
out_2327939745960551937[73] = 0;
|
||||
out_2327939745960551937[74] = 0;
|
||||
out_2327939745960551937[75] = 0;
|
||||
out_2327939745960551937[76] = 0;
|
||||
out_2327939745960551937[77] = 0;
|
||||
out_2327939745960551937[78] = 0;
|
||||
out_2327939745960551937[79] = 0;
|
||||
out_2327939745960551937[80] = 1;
|
||||
void F_fun(double *state, double dt, double *out_7198115545780111359) {
|
||||
out_7198115545780111359[0] = 1;
|
||||
out_7198115545780111359[1] = 0;
|
||||
out_7198115545780111359[2] = 0;
|
||||
out_7198115545780111359[3] = 0;
|
||||
out_7198115545780111359[4] = 0;
|
||||
out_7198115545780111359[5] = 0;
|
||||
out_7198115545780111359[6] = 0;
|
||||
out_7198115545780111359[7] = 0;
|
||||
out_7198115545780111359[8] = 0;
|
||||
out_7198115545780111359[9] = 0;
|
||||
out_7198115545780111359[10] = 1;
|
||||
out_7198115545780111359[11] = 0;
|
||||
out_7198115545780111359[12] = 0;
|
||||
out_7198115545780111359[13] = 0;
|
||||
out_7198115545780111359[14] = 0;
|
||||
out_7198115545780111359[15] = 0;
|
||||
out_7198115545780111359[16] = 0;
|
||||
out_7198115545780111359[17] = 0;
|
||||
out_7198115545780111359[18] = 0;
|
||||
out_7198115545780111359[19] = 0;
|
||||
out_7198115545780111359[20] = 1;
|
||||
out_7198115545780111359[21] = 0;
|
||||
out_7198115545780111359[22] = 0;
|
||||
out_7198115545780111359[23] = 0;
|
||||
out_7198115545780111359[24] = 0;
|
||||
out_7198115545780111359[25] = 0;
|
||||
out_7198115545780111359[26] = 0;
|
||||
out_7198115545780111359[27] = 0;
|
||||
out_7198115545780111359[28] = 0;
|
||||
out_7198115545780111359[29] = 0;
|
||||
out_7198115545780111359[30] = 1;
|
||||
out_7198115545780111359[31] = 0;
|
||||
out_7198115545780111359[32] = 0;
|
||||
out_7198115545780111359[33] = 0;
|
||||
out_7198115545780111359[34] = 0;
|
||||
out_7198115545780111359[35] = 0;
|
||||
out_7198115545780111359[36] = 0;
|
||||
out_7198115545780111359[37] = 0;
|
||||
out_7198115545780111359[38] = 0;
|
||||
out_7198115545780111359[39] = 0;
|
||||
out_7198115545780111359[40] = 1;
|
||||
out_7198115545780111359[41] = 0;
|
||||
out_7198115545780111359[42] = 0;
|
||||
out_7198115545780111359[43] = 0;
|
||||
out_7198115545780111359[44] = 0;
|
||||
out_7198115545780111359[45] = dt*(stiffness_front*(-state[2] - state[3] + state[7])/(mass*state[1]) + (-stiffness_front - stiffness_rear)*state[5]/(mass*state[4]) + (-center_to_front*stiffness_front + center_to_rear*stiffness_rear)*state[6]/(mass*state[4]));
|
||||
out_7198115545780111359[46] = -dt*stiffness_front*(-state[2] - state[3] + state[7])*state[0]/(mass*pow(state[1], 2));
|
||||
out_7198115545780111359[47] = -dt*stiffness_front*state[0]/(mass*state[1]);
|
||||
out_7198115545780111359[48] = -dt*stiffness_front*state[0]/(mass*state[1]);
|
||||
out_7198115545780111359[49] = dt*((-1 - (-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])/(mass*pow(state[4], 2)))*state[6] - (-stiffness_front*state[0] - stiffness_rear*state[0])*state[5]/(mass*pow(state[4], 2)));
|
||||
out_7198115545780111359[50] = dt*(-stiffness_front*state[0] - stiffness_rear*state[0])/(mass*state[4]) + 1;
|
||||
out_7198115545780111359[51] = dt*(-state[4] + (-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])/(mass*state[4]));
|
||||
out_7198115545780111359[52] = dt*stiffness_front*state[0]/(mass*state[1]);
|
||||
out_7198115545780111359[53] = -9.8100000000000005*dt;
|
||||
out_7198115545780111359[54] = dt*(center_to_front*stiffness_front*(-state[2] - state[3] + state[7])/(rotational_inertia*state[1]) + (-center_to_front*stiffness_front + center_to_rear*stiffness_rear)*state[5]/(rotational_inertia*state[4]) + (-pow(center_to_front, 2)*stiffness_front - pow(center_to_rear, 2)*stiffness_rear)*state[6]/(rotational_inertia*state[4]));
|
||||
out_7198115545780111359[55] = -center_to_front*dt*stiffness_front*(-state[2] - state[3] + state[7])*state[0]/(rotational_inertia*pow(state[1], 2));
|
||||
out_7198115545780111359[56] = -center_to_front*dt*stiffness_front*state[0]/(rotational_inertia*state[1]);
|
||||
out_7198115545780111359[57] = -center_to_front*dt*stiffness_front*state[0]/(rotational_inertia*state[1]);
|
||||
out_7198115545780111359[58] = dt*(-(-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])*state[5]/(rotational_inertia*pow(state[4], 2)) - (-pow(center_to_front, 2)*stiffness_front*state[0] - pow(center_to_rear, 2)*stiffness_rear*state[0])*state[6]/(rotational_inertia*pow(state[4], 2)));
|
||||
out_7198115545780111359[59] = dt*(-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])/(rotational_inertia*state[4]);
|
||||
out_7198115545780111359[60] = dt*(-pow(center_to_front, 2)*stiffness_front*state[0] - pow(center_to_rear, 2)*stiffness_rear*state[0])/(rotational_inertia*state[4]) + 1;
|
||||
out_7198115545780111359[61] = center_to_front*dt*stiffness_front*state[0]/(rotational_inertia*state[1]);
|
||||
out_7198115545780111359[62] = 0;
|
||||
out_7198115545780111359[63] = 0;
|
||||
out_7198115545780111359[64] = 0;
|
||||
out_7198115545780111359[65] = 0;
|
||||
out_7198115545780111359[66] = 0;
|
||||
out_7198115545780111359[67] = 0;
|
||||
out_7198115545780111359[68] = 0;
|
||||
out_7198115545780111359[69] = 0;
|
||||
out_7198115545780111359[70] = 1;
|
||||
out_7198115545780111359[71] = 0;
|
||||
out_7198115545780111359[72] = 0;
|
||||
out_7198115545780111359[73] = 0;
|
||||
out_7198115545780111359[74] = 0;
|
||||
out_7198115545780111359[75] = 0;
|
||||
out_7198115545780111359[76] = 0;
|
||||
out_7198115545780111359[77] = 0;
|
||||
out_7198115545780111359[78] = 0;
|
||||
out_7198115545780111359[79] = 0;
|
||||
out_7198115545780111359[80] = 1;
|
||||
}
|
||||
void h_25(double *state, double *unused, double *out_8734450240234959528) {
|
||||
out_8734450240234959528[0] = state[6];
|
||||
void h_25(double *state, double *unused, double *out_5441109048213736058) {
|
||||
out_5441109048213736058[0] = state[6];
|
||||
}
|
||||
void H_25(double *state, double *unused, double *out_6133161197384342845) {
|
||||
out_6133161197384342845[0] = 0;
|
||||
out_6133161197384342845[1] = 0;
|
||||
out_6133161197384342845[2] = 0;
|
||||
out_6133161197384342845[3] = 0;
|
||||
out_6133161197384342845[4] = 0;
|
||||
out_6133161197384342845[5] = 0;
|
||||
out_6133161197384342845[6] = 1;
|
||||
out_6133161197384342845[7] = 0;
|
||||
out_6133161197384342845[8] = 0;
|
||||
void H_25(double *state, double *unused, double *out_4766949399557973891) {
|
||||
out_4766949399557973891[0] = 0;
|
||||
out_4766949399557973891[1] = 0;
|
||||
out_4766949399557973891[2] = 0;
|
||||
out_4766949399557973891[3] = 0;
|
||||
out_4766949399557973891[4] = 0;
|
||||
out_4766949399557973891[5] = 0;
|
||||
out_4766949399557973891[6] = 1;
|
||||
out_4766949399557973891[7] = 0;
|
||||
out_4766949399557973891[8] = 0;
|
||||
}
|
||||
void h_24(double *state, double *unused, double *out_8806581475742121932) {
|
||||
out_8806581475742121932[0] = state[4];
|
||||
out_8806581475742121932[1] = state[5];
|
||||
void h_24(double *state, double *unused, double *out_2797667639827089179) {
|
||||
out_2797667639827089179[0] = state[4];
|
||||
out_2797667639827089179[1] = state[5];
|
||||
}
|
||||
void H_24(double *state, double *unused, double *out_5738011069733690670) {
|
||||
out_5738011069733690670[0] = 0;
|
||||
out_5738011069733690670[1] = 0;
|
||||
out_5738011069733690670[2] = 0;
|
||||
out_5738011069733690670[3] = 0;
|
||||
out_5738011069733690670[4] = 1;
|
||||
out_5738011069733690670[5] = 0;
|
||||
out_5738011069733690670[6] = 0;
|
||||
out_5738011069733690670[7] = 0;
|
||||
out_5738011069733690670[8] = 0;
|
||||
out_5738011069733690670[9] = 0;
|
||||
out_5738011069733690670[10] = 0;
|
||||
out_5738011069733690670[11] = 0;
|
||||
out_5738011069733690670[12] = 0;
|
||||
out_5738011069733690670[13] = 0;
|
||||
out_5738011069733690670[14] = 1;
|
||||
out_5738011069733690670[15] = 0;
|
||||
out_5738011069733690670[16] = 0;
|
||||
out_5738011069733690670[17] = 0;
|
||||
void H_24(double *state, double *unused, double *out_4461115786511221334) {
|
||||
out_4461115786511221334[0] = 0;
|
||||
out_4461115786511221334[1] = 0;
|
||||
out_4461115786511221334[2] = 0;
|
||||
out_4461115786511221334[3] = 0;
|
||||
out_4461115786511221334[4] = 1;
|
||||
out_4461115786511221334[5] = 0;
|
||||
out_4461115786511221334[6] = 0;
|
||||
out_4461115786511221334[7] = 0;
|
||||
out_4461115786511221334[8] = 0;
|
||||
out_4461115786511221334[9] = 0;
|
||||
out_4461115786511221334[10] = 0;
|
||||
out_4461115786511221334[11] = 0;
|
||||
out_4461115786511221334[12] = 0;
|
||||
out_4461115786511221334[13] = 0;
|
||||
out_4461115786511221334[14] = 1;
|
||||
out_4461115786511221334[15] = 0;
|
||||
out_4461115786511221334[16] = 0;
|
||||
out_4461115786511221334[17] = 0;
|
||||
}
|
||||
void h_30(double *state, double *unused, double *out_971184983414287087) {
|
||||
out_971184983414287087[0] = state[4];
|
||||
void h_30(double *state, double *unused, double *out_1188271489450022901) {
|
||||
out_1188271489450022901[0] = state[4];
|
||||
}
|
||||
void H_30(double *state, double *unused, double *out_5396892534833592016) {
|
||||
out_5396892534833592016[0] = 0;
|
||||
out_5396892534833592016[1] = 0;
|
||||
out_5396892534833592016[2] = 0;
|
||||
out_5396892534833592016[3] = 0;
|
||||
out_5396892534833592016[4] = 1;
|
||||
out_5396892534833592016[5] = 0;
|
||||
out_5396892534833592016[6] = 0;
|
||||
out_5396892534833592016[7] = 0;
|
||||
out_5396892534833592016[8] = 0;
|
||||
void H_30(double *state, double *unused, double *out_9152098344023969527) {
|
||||
out_9152098344023969527[0] = 0;
|
||||
out_9152098344023969527[1] = 0;
|
||||
out_9152098344023969527[2] = 0;
|
||||
out_9152098344023969527[3] = 0;
|
||||
out_9152098344023969527[4] = 1;
|
||||
out_9152098344023969527[5] = 0;
|
||||
out_9152098344023969527[6] = 0;
|
||||
out_9152098344023969527[7] = 0;
|
||||
out_9152098344023969527[8] = 0;
|
||||
}
|
||||
void h_26(double *state, double *unused, double *out_1416855028350640224) {
|
||||
out_1416855028350640224[0] = state[7];
|
||||
void h_26(double *state, double *unused, double *out_6530138725336053464) {
|
||||
out_6530138725336053464[0] = state[7];
|
||||
}
|
||||
void H_26(double *state, double *unused, double *out_2391657878510286621) {
|
||||
out_2391657878510286621[0] = 0;
|
||||
out_2391657878510286621[1] = 0;
|
||||
out_2391657878510286621[2] = 0;
|
||||
out_2391657878510286621[3] = 0;
|
||||
out_2391657878510286621[4] = 0;
|
||||
out_2391657878510286621[5] = 0;
|
||||
out_2391657878510286621[6] = 0;
|
||||
out_2391657878510286621[7] = 1;
|
||||
out_2391657878510286621[8] = 0;
|
||||
void H_26(double *state, double *unused, double *out_8508452718432030115) {
|
||||
out_8508452718432030115[0] = 0;
|
||||
out_8508452718432030115[1] = 0;
|
||||
out_8508452718432030115[2] = 0;
|
||||
out_8508452718432030115[3] = 0;
|
||||
out_8508452718432030115[4] = 0;
|
||||
out_8508452718432030115[5] = 0;
|
||||
out_8508452718432030115[6] = 0;
|
||||
out_8508452718432030115[7] = 1;
|
||||
out_8508452718432030115[8] = 0;
|
||||
}
|
||||
void h_27(double *state, double *unused, double *out_5511166866350839857) {
|
||||
out_5511166866350839857[0] = state[3];
|
||||
void h_27(double *state, double *unused, double *out_8003961757834969913) {
|
||||
out_8003961757834969913[0] = state[3];
|
||||
}
|
||||
void H_27(double *state, double *unused, double *out_7571655846634016927) {
|
||||
out_7571655846634016927[0] = 0;
|
||||
out_7571655846634016927[1] = 0;
|
||||
out_7571655846634016927[2] = 0;
|
||||
out_7571655846634016927[3] = 1;
|
||||
out_7571655846634016927[4] = 0;
|
||||
out_7571655846634016927[5] = 0;
|
||||
out_7571655846634016927[6] = 0;
|
||||
out_7571655846634016927[7] = 0;
|
||||
out_7571655846634016927[8] = 0;
|
||||
void H_27(double *state, double *unused, double *out_6977335032223544616) {
|
||||
out_6977335032223544616[0] = 0;
|
||||
out_6977335032223544616[1] = 0;
|
||||
out_6977335032223544616[2] = 0;
|
||||
out_6977335032223544616[3] = 1;
|
||||
out_6977335032223544616[4] = 0;
|
||||
out_6977335032223544616[5] = 0;
|
||||
out_6977335032223544616[6] = 0;
|
||||
out_6977335032223544616[7] = 0;
|
||||
out_6977335032223544616[8] = 0;
|
||||
}
|
||||
void h_29(double *state, double *unused, double *out_5235972804066333968) {
|
||||
out_5235972804066333968[0] = state[1];
|
||||
void h_29(double *state, double *unused, double *out_1416133831358674750) {
|
||||
out_1416133831358674750[0] = state[1];
|
||||
}
|
||||
void H_29(double *state, double *unused, double *out_9161725500205983656) {
|
||||
out_9161725500205983656[0] = 0;
|
||||
out_9161725500205983656[1] = 1;
|
||||
out_9161725500205983656[2] = 0;
|
||||
out_9161725500205983656[3] = 0;
|
||||
out_9161725500205983656[4] = 0;
|
||||
out_9161725500205983656[5] = 0;
|
||||
out_9161725500205983656[6] = 0;
|
||||
out_9161725500205983656[7] = 0;
|
||||
out_9161725500205983656[8] = 0;
|
||||
void H_29(double *state, double *unused, double *out_8784414385371189905) {
|
||||
out_8784414385371189905[0] = 0;
|
||||
out_8784414385371189905[1] = 1;
|
||||
out_8784414385371189905[2] = 0;
|
||||
out_8784414385371189905[3] = 0;
|
||||
out_8784414385371189905[4] = 0;
|
||||
out_8784414385371189905[5] = 0;
|
||||
out_8784414385371189905[6] = 0;
|
||||
out_8784414385371189905[7] = 0;
|
||||
out_8784414385371189905[8] = 0;
|
||||
}
|
||||
void h_28(double *state, double *unused, double *out_7891338238985058206) {
|
||||
out_7891338238985058206[0] = state[0];
|
||||
void h_28(double *state, double *unused, double *out_7400645873158492396) {
|
||||
out_7400645873158492396[0] = state[0];
|
||||
}
|
||||
void H_28(double *state, double *unused, double *out_4079326483136453082) {
|
||||
out_4079326483136453082[0] = 1;
|
||||
out_4079326483136453082[1] = 0;
|
||||
out_4079326483136453082[2] = 0;
|
||||
out_4079326483136453082[3] = 0;
|
||||
out_4079326483136453082[4] = 0;
|
||||
out_4079326483136453082[5] = 0;
|
||||
out_4079326483136453082[6] = 0;
|
||||
out_4079326483136453082[7] = 0;
|
||||
out_4079326483136453082[8] = 0;
|
||||
void H_28(double *state, double *unused, double *out_6820784113805863654) {
|
||||
out_6820784113805863654[0] = 1;
|
||||
out_6820784113805863654[1] = 0;
|
||||
out_6820784113805863654[2] = 0;
|
||||
out_6820784113805863654[3] = 0;
|
||||
out_6820784113805863654[4] = 0;
|
||||
out_6820784113805863654[5] = 0;
|
||||
out_6820784113805863654[6] = 0;
|
||||
out_6820784113805863654[7] = 0;
|
||||
out_6820784113805863654[8] = 0;
|
||||
}
|
||||
void h_31(double *state, double *unused, double *out_8694811794428211892) {
|
||||
out_8694811794428211892[0] = state[8];
|
||||
void h_31(double *state, double *unused, double *out_1803659519959713708) {
|
||||
out_1803659519959713708[0] = state[8];
|
||||
}
|
||||
void H_31(double *state, double *unused, double *out_6163807159261303273) {
|
||||
out_6163807159261303273[0] = 0;
|
||||
out_6163807159261303273[1] = 0;
|
||||
out_6163807159261303273[2] = 0;
|
||||
out_6163807159261303273[3] = 0;
|
||||
out_6163807159261303273[4] = 0;
|
||||
out_6163807159261303273[5] = 0;
|
||||
out_6163807159261303273[6] = 0;
|
||||
out_6163807159261303273[7] = 0;
|
||||
out_6163807159261303273[8] = 1;
|
||||
void H_31(double *state, double *unused, double *out_9134660820665381591) {
|
||||
out_9134660820665381591[0] = 0;
|
||||
out_9134660820665381591[1] = 0;
|
||||
out_9134660820665381591[2] = 0;
|
||||
out_9134660820665381591[3] = 0;
|
||||
out_9134660820665381591[4] = 0;
|
||||
out_9134660820665381591[5] = 0;
|
||||
out_9134660820665381591[6] = 0;
|
||||
out_9134660820665381591[7] = 0;
|
||||
out_9134660820665381591[8] = 1;
|
||||
}
|
||||
#include <eigen3/Eigen/Dense>
|
||||
#include <iostream>
|
||||
@@ -518,68 +518,68 @@ void car_update_28(double *in_x, double *in_P, double *in_z, double *in_R, doubl
|
||||
void car_update_31(double *in_x, double *in_P, double *in_z, double *in_R, double *in_ea) {
|
||||
update<1, 3, 0>(in_x, in_P, h_31, H_31, NULL, in_z, in_R, in_ea, MAHA_THRESH_31);
|
||||
}
|
||||
void car_err_fun(double *nom_x, double *delta_x, double *out_205475353292099530) {
|
||||
err_fun(nom_x, delta_x, out_205475353292099530);
|
||||
void car_err_fun(double *nom_x, double *delta_x, double *out_5070392900769873758) {
|
||||
err_fun(nom_x, delta_x, out_5070392900769873758);
|
||||
}
|
||||
void car_inv_err_fun(double *nom_x, double *true_x, double *out_6859088956493527341) {
|
||||
inv_err_fun(nom_x, true_x, out_6859088956493527341);
|
||||
void car_inv_err_fun(double *nom_x, double *true_x, double *out_3742760733154016051) {
|
||||
inv_err_fun(nom_x, true_x, out_3742760733154016051);
|
||||
}
|
||||
void car_H_mod_fun(double *state, double *out_8201686483145055601) {
|
||||
H_mod_fun(state, out_8201686483145055601);
|
||||
void car_H_mod_fun(double *state, double *out_5758456641177717920) {
|
||||
H_mod_fun(state, out_5758456641177717920);
|
||||
}
|
||||
void car_f_fun(double *state, double dt, double *out_2204957494938772746) {
|
||||
f_fun(state, dt, out_2204957494938772746);
|
||||
void car_f_fun(double *state, double dt, double *out_730595102125265078) {
|
||||
f_fun(state, dt, out_730595102125265078);
|
||||
}
|
||||
void car_F_fun(double *state, double dt, double *out_2327939745960551937) {
|
||||
F_fun(state, dt, out_2327939745960551937);
|
||||
void car_F_fun(double *state, double dt, double *out_7198115545780111359) {
|
||||
F_fun(state, dt, out_7198115545780111359);
|
||||
}
|
||||
void car_h_25(double *state, double *unused, double *out_8734450240234959528) {
|
||||
h_25(state, unused, out_8734450240234959528);
|
||||
void car_h_25(double *state, double *unused, double *out_5441109048213736058) {
|
||||
h_25(state, unused, out_5441109048213736058);
|
||||
}
|
||||
void car_H_25(double *state, double *unused, double *out_6133161197384342845) {
|
||||
H_25(state, unused, out_6133161197384342845);
|
||||
void car_H_25(double *state, double *unused, double *out_4766949399557973891) {
|
||||
H_25(state, unused, out_4766949399557973891);
|
||||
}
|
||||
void car_h_24(double *state, double *unused, double *out_8806581475742121932) {
|
||||
h_24(state, unused, out_8806581475742121932);
|
||||
void car_h_24(double *state, double *unused, double *out_2797667639827089179) {
|
||||
h_24(state, unused, out_2797667639827089179);
|
||||
}
|
||||
void car_H_24(double *state, double *unused, double *out_5738011069733690670) {
|
||||
H_24(state, unused, out_5738011069733690670);
|
||||
void car_H_24(double *state, double *unused, double *out_4461115786511221334) {
|
||||
H_24(state, unused, out_4461115786511221334);
|
||||
}
|
||||
void car_h_30(double *state, double *unused, double *out_971184983414287087) {
|
||||
h_30(state, unused, out_971184983414287087);
|
||||
void car_h_30(double *state, double *unused, double *out_1188271489450022901) {
|
||||
h_30(state, unused, out_1188271489450022901);
|
||||
}
|
||||
void car_H_30(double *state, double *unused, double *out_5396892534833592016) {
|
||||
H_30(state, unused, out_5396892534833592016);
|
||||
void car_H_30(double *state, double *unused, double *out_9152098344023969527) {
|
||||
H_30(state, unused, out_9152098344023969527);
|
||||
}
|
||||
void car_h_26(double *state, double *unused, double *out_1416855028350640224) {
|
||||
h_26(state, unused, out_1416855028350640224);
|
||||
void car_h_26(double *state, double *unused, double *out_6530138725336053464) {
|
||||
h_26(state, unused, out_6530138725336053464);
|
||||
}
|
||||
void car_H_26(double *state, double *unused, double *out_2391657878510286621) {
|
||||
H_26(state, unused, out_2391657878510286621);
|
||||
void car_H_26(double *state, double *unused, double *out_8508452718432030115) {
|
||||
H_26(state, unused, out_8508452718432030115);
|
||||
}
|
||||
void car_h_27(double *state, double *unused, double *out_5511166866350839857) {
|
||||
h_27(state, unused, out_5511166866350839857);
|
||||
void car_h_27(double *state, double *unused, double *out_8003961757834969913) {
|
||||
h_27(state, unused, out_8003961757834969913);
|
||||
}
|
||||
void car_H_27(double *state, double *unused, double *out_7571655846634016927) {
|
||||
H_27(state, unused, out_7571655846634016927);
|
||||
void car_H_27(double *state, double *unused, double *out_6977335032223544616) {
|
||||
H_27(state, unused, out_6977335032223544616);
|
||||
}
|
||||
void car_h_29(double *state, double *unused, double *out_5235972804066333968) {
|
||||
h_29(state, unused, out_5235972804066333968);
|
||||
void car_h_29(double *state, double *unused, double *out_1416133831358674750) {
|
||||
h_29(state, unused, out_1416133831358674750);
|
||||
}
|
||||
void car_H_29(double *state, double *unused, double *out_9161725500205983656) {
|
||||
H_29(state, unused, out_9161725500205983656);
|
||||
void car_H_29(double *state, double *unused, double *out_8784414385371189905) {
|
||||
H_29(state, unused, out_8784414385371189905);
|
||||
}
|
||||
void car_h_28(double *state, double *unused, double *out_7891338238985058206) {
|
||||
h_28(state, unused, out_7891338238985058206);
|
||||
void car_h_28(double *state, double *unused, double *out_7400645873158492396) {
|
||||
h_28(state, unused, out_7400645873158492396);
|
||||
}
|
||||
void car_H_28(double *state, double *unused, double *out_4079326483136453082) {
|
||||
H_28(state, unused, out_4079326483136453082);
|
||||
void car_H_28(double *state, double *unused, double *out_6820784113805863654) {
|
||||
H_28(state, unused, out_6820784113805863654);
|
||||
}
|
||||
void car_h_31(double *state, double *unused, double *out_8694811794428211892) {
|
||||
h_31(state, unused, out_8694811794428211892);
|
||||
void car_h_31(double *state, double *unused, double *out_1803659519959713708) {
|
||||
h_31(state, unused, out_1803659519959713708);
|
||||
}
|
||||
void car_H_31(double *state, double *unused, double *out_6163807159261303273) {
|
||||
H_31(state, unused, out_6163807159261303273);
|
||||
void car_H_31(double *state, double *unused, double *out_9134660820665381591) {
|
||||
H_31(state, unused, out_9134660820665381591);
|
||||
}
|
||||
void car_predict(double *in_x, double *in_P, double *in_Q, double dt) {
|
||||
predict(in_x, in_P, in_Q, dt);
|
||||
|
||||
@@ -9,27 +9,27 @@ void car_update_27(double *in_x, double *in_P, double *in_z, double *in_R, doubl
|
||||
void car_update_29(double *in_x, double *in_P, double *in_z, double *in_R, double *in_ea);
|
||||
void car_update_28(double *in_x, double *in_P, double *in_z, double *in_R, double *in_ea);
|
||||
void car_update_31(double *in_x, double *in_P, double *in_z, double *in_R, double *in_ea);
|
||||
void car_err_fun(double *nom_x, double *delta_x, double *out_205475353292099530);
|
||||
void car_inv_err_fun(double *nom_x, double *true_x, double *out_6859088956493527341);
|
||||
void car_H_mod_fun(double *state, double *out_8201686483145055601);
|
||||
void car_f_fun(double *state, double dt, double *out_2204957494938772746);
|
||||
void car_F_fun(double *state, double dt, double *out_2327939745960551937);
|
||||
void car_h_25(double *state, double *unused, double *out_8734450240234959528);
|
||||
void car_H_25(double *state, double *unused, double *out_6133161197384342845);
|
||||
void car_h_24(double *state, double *unused, double *out_8806581475742121932);
|
||||
void car_H_24(double *state, double *unused, double *out_5738011069733690670);
|
||||
void car_h_30(double *state, double *unused, double *out_971184983414287087);
|
||||
void car_H_30(double *state, double *unused, double *out_5396892534833592016);
|
||||
void car_h_26(double *state, double *unused, double *out_1416855028350640224);
|
||||
void car_H_26(double *state, double *unused, double *out_2391657878510286621);
|
||||
void car_h_27(double *state, double *unused, double *out_5511166866350839857);
|
||||
void car_H_27(double *state, double *unused, double *out_7571655846634016927);
|
||||
void car_h_29(double *state, double *unused, double *out_5235972804066333968);
|
||||
void car_H_29(double *state, double *unused, double *out_9161725500205983656);
|
||||
void car_h_28(double *state, double *unused, double *out_7891338238985058206);
|
||||
void car_H_28(double *state, double *unused, double *out_4079326483136453082);
|
||||
void car_h_31(double *state, double *unused, double *out_8694811794428211892);
|
||||
void car_H_31(double *state, double *unused, double *out_6163807159261303273);
|
||||
void car_err_fun(double *nom_x, double *delta_x, double *out_5070392900769873758);
|
||||
void car_inv_err_fun(double *nom_x, double *true_x, double *out_3742760733154016051);
|
||||
void car_H_mod_fun(double *state, double *out_5758456641177717920);
|
||||
void car_f_fun(double *state, double dt, double *out_730595102125265078);
|
||||
void car_F_fun(double *state, double dt, double *out_7198115545780111359);
|
||||
void car_h_25(double *state, double *unused, double *out_5441109048213736058);
|
||||
void car_H_25(double *state, double *unused, double *out_4766949399557973891);
|
||||
void car_h_24(double *state, double *unused, double *out_2797667639827089179);
|
||||
void car_H_24(double *state, double *unused, double *out_4461115786511221334);
|
||||
void car_h_30(double *state, double *unused, double *out_1188271489450022901);
|
||||
void car_H_30(double *state, double *unused, double *out_9152098344023969527);
|
||||
void car_h_26(double *state, double *unused, double *out_6530138725336053464);
|
||||
void car_H_26(double *state, double *unused, double *out_8508452718432030115);
|
||||
void car_h_27(double *state, double *unused, double *out_8003961757834969913);
|
||||
void car_H_27(double *state, double *unused, double *out_6977335032223544616);
|
||||
void car_h_29(double *state, double *unused, double *out_1416133831358674750);
|
||||
void car_H_29(double *state, double *unused, double *out_8784414385371189905);
|
||||
void car_h_28(double *state, double *unused, double *out_7400645873158492396);
|
||||
void car_H_28(double *state, double *unused, double *out_6820784113805863654);
|
||||
void car_h_31(double *state, double *unused, double *out_1803659519959713708);
|
||||
void car_H_31(double *state, double *unused, double *out_9134660820665381591);
|
||||
void car_predict(double *in_x, double *in_P, double *in_Q, double dt);
|
||||
void car_set_mass(double x);
|
||||
void car_set_rotational_inertia(double x);
|
||||
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -5,18 +5,18 @@ void pose_update_4(double *in_x, double *in_P, double *in_z, double *in_R, doubl
|
||||
void pose_update_10(double *in_x, double *in_P, double *in_z, double *in_R, double *in_ea);
|
||||
void pose_update_13(double *in_x, double *in_P, double *in_z, double *in_R, double *in_ea);
|
||||
void pose_update_14(double *in_x, double *in_P, double *in_z, double *in_R, double *in_ea);
|
||||
void pose_err_fun(double *nom_x, double *delta_x, double *out_7491504487925958906);
|
||||
void pose_inv_err_fun(double *nom_x, double *true_x, double *out_8173082431785614969);
|
||||
void pose_H_mod_fun(double *state, double *out_3955896825213980969);
|
||||
void pose_f_fun(double *state, double dt, double *out_6226431844178190638);
|
||||
void pose_F_fun(double *state, double dt, double *out_5590858509180719260);
|
||||
void pose_h_4(double *state, double *unused, double *out_8311116516369893334);
|
||||
void pose_H_4(double *state, double *unused, double *out_1196621559825099766);
|
||||
void pose_h_10(double *state, double *unused, double *out_8024633733467632830);
|
||||
void pose_H_10(double *state, double *unused, double *out_9211636060607930666);
|
||||
void pose_h_13(double *state, double *unused, double *out_5246243153983473200);
|
||||
void pose_H_13(double *state, double *unused, double *out_4408895385157432567);
|
||||
void pose_h_14(double *state, double *unused, double *out_2666716766060469740);
|
||||
void pose_H_14(double *state, double *unused, double *out_5159862416164584295);
|
||||
void pose_err_fun(double *nom_x, double *delta_x, double *out_8299703332077589328);
|
||||
void pose_inv_err_fun(double *nom_x, double *true_x, double *out_4608058237383429265);
|
||||
void pose_H_mod_fun(double *state, double *out_7885453398379249088);
|
||||
void pose_f_fun(double *state, double dt, double *out_1688144950946873165);
|
||||
void pose_F_fun(double *state, double dt, double *out_4851283621730691038);
|
||||
void pose_h_4(double *state, double *unused, double *out_2329133011647763937);
|
||||
void pose_H_4(double *state, double *unused, double *out_8639614400433961695);
|
||||
void pose_h_10(double *state, double *unused, double *out_3294648635274985008);
|
||||
void pose_H_10(double *state, double *unused, double *out_3780608178453222524);
|
||||
void pose_h_13(double *state, double *unused, double *out_8905113933939181200);
|
||||
void pose_H_13(double *state, double *unused, double *out_6594855847943257120);
|
||||
void pose_h_14(double *state, double *unused, double *out_8324071568749841126);
|
||||
void pose_H_14(double *state, double *unused, double *out_5843888816936105392);
|
||||
void pose_predict(double *in_x, double *in_P, double *in_Q, double dt);
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -29,12 +29,6 @@ enum SpiError {
|
||||
|
||||
const unsigned int SPI_ACK_TIMEOUT = 500; // milliseconds
|
||||
const std::string SPI_DEVICE = "/dev/spidev0.0";
|
||||
// TODO: fix SPI turnaround synchronization at the protocol level.
|
||||
static uint64_t spi_last_bus_activity_ns = 0; // protected by hw_lock
|
||||
|
||||
static void wait_for_spi_turnaround(uint64_t start_ns) {
|
||||
while ((nanos_since_boot() - start_ns) < 400000) {}
|
||||
}
|
||||
|
||||
class LockEx {
|
||||
public:
|
||||
@@ -325,8 +319,6 @@ int PandaSpiHandle::spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx
|
||||
assert(tx_len < SPI_BUF_SIZE);
|
||||
assert(max_rx_len < SPI_BUF_SIZE);
|
||||
|
||||
wait_for_spi_turnaround(spi_last_bus_activity_ns);
|
||||
|
||||
xfer_count++;
|
||||
header = {
|
||||
.sync = SPI_SYNC,
|
||||
@@ -355,7 +347,6 @@ int PandaSpiHandle::spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx
|
||||
if (ret < 0) {
|
||||
goto fail;
|
||||
}
|
||||
wait_for_spi_turnaround(nanos_since_boot());
|
||||
|
||||
// Send data
|
||||
if (tx_data != NULL) {
|
||||
@@ -398,7 +389,6 @@ int PandaSpiHandle::spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx
|
||||
memcpy(rx_data, rx_buf + 3, rx_data_len);
|
||||
}
|
||||
|
||||
spi_last_bus_activity_ns = nanos_since_boot();
|
||||
return rx_data_len;
|
||||
|
||||
fail:
|
||||
@@ -413,7 +403,6 @@ fail:
|
||||
}
|
||||
}
|
||||
|
||||
spi_last_bus_activity_ns = nanos_since_boot();
|
||||
if (ret >= 0) ret = -1;
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,15 @@ from openpilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPl
|
||||
from openpilot.selfdrive.controls.radard import _LEAD_ACCEL_TAU
|
||||
|
||||
|
||||
class PlannerSM(dict):
|
||||
def __init__(self, radar_frame: int, services: dict):
|
||||
super().__init__(services)
|
||||
self.frame = radar_frame
|
||||
self.logMonoTime = {"radarState": radar_frame}
|
||||
self.valid = {"radarState": True}
|
||||
self.alive = {"radarState": True}
|
||||
|
||||
|
||||
class Plant:
|
||||
messaging_initialized = False
|
||||
|
||||
@@ -132,7 +141,7 @@ class Plant:
|
||||
car_control.carControl.orientationNED = [0., float(pitch), 0.]
|
||||
|
||||
# ******** get controlsState messages for plotting ***
|
||||
sm = {'radarState': radar.radarState,
|
||||
sm = PlannerSM(self.rk.frame, {'radarState': radar.radarState,
|
||||
'carState': car_state.carState,
|
||||
'carControl': car_control.carControl,
|
||||
'controlsState': control.controlsState,
|
||||
@@ -141,7 +150,7 @@ class Plant:
|
||||
'modelV2': model.modelV2,
|
||||
'carStateSP': car_state_sp.carStateSP,
|
||||
'liveMapDataSP': live_map_data_sp.liveMapDataSP,
|
||||
'gpsLocation': gps_data.gpsLocation}
|
||||
'gpsLocation': gps_data.gpsLocation})
|
||||
self.planner.update(sm)
|
||||
self.acceleration = self.planner.output_a_target
|
||||
if self.planner.output_should_stop:
|
||||
|
||||
@@ -27,6 +27,12 @@ DESCRIPTIONS = {
|
||||
"In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with " +
|
||||
"your steering wheel distance button."
|
||||
),
|
||||
"AccelPersonalityEnabled": tr_noop(
|
||||
"Lets you choose how sunnypilot starts, catches up, and settles at the cruise speed. Emergency braking and stopping are unchanged."
|
||||
),
|
||||
"AccelPersonality": tr_noop(
|
||||
"Eco is gentlest, Normal balances a prompt start with smooth catch-up, and Sport is more responsive."
|
||||
),
|
||||
"IsLdwEnabled": tr_noop(
|
||||
"Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line " +
|
||||
"without a turn signal activated while driving over 31 mph (50 km/h)."
|
||||
@@ -106,6 +112,24 @@ class TogglesLayout(Widget):
|
||||
icon="speed_limit.png"
|
||||
)
|
||||
|
||||
self._accel_controller_enabled = toggle_item(
|
||||
lambda: tr("Enable Accel Controller"),
|
||||
lambda: tr(DESCRIPTIONS["AccelPersonalityEnabled"]),
|
||||
self._params.get_bool("AccelPersonalityEnabled"),
|
||||
callback=self._set_accel_controller_enabled,
|
||||
icon="speed_limit.png",
|
||||
)
|
||||
|
||||
self._accel_personality_setting = multiple_button_item(
|
||||
lambda: tr("Acceleration Profile"),
|
||||
lambda: tr(DESCRIPTIONS["AccelPersonality"]),
|
||||
buttons=[lambda: tr("Eco"), lambda: tr("Normal"), lambda: tr("Sport")],
|
||||
button_width=300,
|
||||
callback=self._set_accel_personality,
|
||||
selected_index=self._params.get("AccelPersonality", return_default=True),
|
||||
icon="speed_limit.png"
|
||||
)
|
||||
|
||||
self._toggles = {}
|
||||
self._locked_toggles = set()
|
||||
for param, (title, desc, icon, needs_restart) in self._toggle_defs.items():
|
||||
@@ -135,9 +159,11 @@ class TogglesLayout(Widget):
|
||||
|
||||
self._toggles[param] = toggle
|
||||
|
||||
# insert longitudinal personality after NDOG toggle
|
||||
# insert longitudinal personality and Accel Controller settings after NDOG toggle
|
||||
if param == "DisengageOnAccelerator":
|
||||
self._toggles["LongitudinalPersonality"] = self._long_personality_setting
|
||||
self._toggles["AccelPersonalityEnabled"] = self._accel_controller_enabled
|
||||
self._toggles["AccelPersonality"] = self._accel_personality_setting
|
||||
|
||||
self._update_experimental_mode_icon()
|
||||
self._scroller = Scroller(list(self._toggles.values()), line_separator=True, spacing=0)
|
||||
@@ -158,6 +184,7 @@ class TogglesLayout(Widget):
|
||||
|
||||
def _update_toggles(self):
|
||||
ui_state.update_params()
|
||||
accel_controller_enabled = self._params.get_bool("AccelPersonalityEnabled")
|
||||
|
||||
e2e_description = tr(
|
||||
"sunnypilot defaults to driving in chill mode. Experimental mode enables alpha-level features that aren't ready for chill mode. " +
|
||||
@@ -176,11 +203,15 @@ class TogglesLayout(Widget):
|
||||
self._toggles["ExperimentalMode"].action_item.set_enabled(True)
|
||||
self._toggles["ExperimentalMode"].set_description(e2e_description)
|
||||
self._long_personality_setting.action_item.set_enabled(True)
|
||||
self._accel_controller_enabled.action_item.set_enabled(True)
|
||||
self._accel_personality_setting.action_item.set_enabled(True)
|
||||
else:
|
||||
# no long for now
|
||||
self._toggles["ExperimentalMode"].action_item.set_enabled(False)
|
||||
self._toggles["ExperimentalMode"].action_item.set_state(False)
|
||||
self._long_personality_setting.action_item.set_enabled(False)
|
||||
self._accel_controller_enabled.action_item.set_enabled(False)
|
||||
self._accel_personality_setting.action_item.set_enabled(False)
|
||||
self._params.remove("ExperimentalMode")
|
||||
|
||||
unavailable = tr("Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control.")
|
||||
@@ -203,6 +234,8 @@ class TogglesLayout(Widget):
|
||||
# refresh toggles from params to mirror external changes
|
||||
for param in self._toggle_defs:
|
||||
self._toggles[param].action_item.set_state(self._params.get_bool(param))
|
||||
self._accel_controller_enabled.action_item.set_state(accel_controller_enabled)
|
||||
self._accel_personality_setting.action_item.set_selected_button(self._params.get("AccelPersonality", return_default=True))
|
||||
|
||||
# these toggles need restart, block while engaged
|
||||
for toggle_def in self._toggle_defs:
|
||||
@@ -247,3 +280,9 @@ class TogglesLayout(Widget):
|
||||
|
||||
def _set_longitudinal_personality(self, button_index: int):
|
||||
self._params.put("LongitudinalPersonality", button_index, block=True)
|
||||
|
||||
def _set_accel_personality(self, button_index: int):
|
||||
self._params.put("AccelPersonality", button_index, block=True)
|
||||
|
||||
def _set_accel_controller_enabled(self, state: bool):
|
||||
self._params.put_bool("AccelPersonalityEnabled", state, block=True)
|
||||
|
||||
@@ -14,6 +14,7 @@ from openpilot.system.ui.lib.application import gui_app
|
||||
if gui_app.sunnypilot_ui():
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.settings import SettingsLayoutSP as SettingsLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.home import MiciHomeLayoutSP as MiciHomeLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onroad import OnroadViewContainerSP as AugmentedRoadView
|
||||
|
||||
ONROAD_DELAY = 2.5 # seconds
|
||||
|
||||
@@ -72,6 +73,9 @@ class MiciMainLayout(Scroller):
|
||||
# For scroll_to
|
||||
return self._body_onroad_layout if ui_state.is_body else self._car_onroad_layout
|
||||
|
||||
def _should_auto_scroll_to_onroad(self) -> bool:
|
||||
return True
|
||||
|
||||
def _setup_callbacks(self):
|
||||
self._home_layout.set_callbacks(
|
||||
on_settings=lambda: gui_app.push_widget(self._settings_layout),
|
||||
@@ -122,13 +126,15 @@ class MiciMainLayout(Scroller):
|
||||
|
||||
# FIXME: these two pops can interrupt user interacting in the settings
|
||||
if self._onroad_time_delay is not None and rl.get_time() - self._onroad_time_delay >= ONROAD_DELAY:
|
||||
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
|
||||
if not gui_app.sunnypilot_ui() or self._should_auto_scroll_to_onroad():
|
||||
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
|
||||
self._onroad_time_delay = None
|
||||
|
||||
# When car leaves standstill, pop nav stack and scroll to onroad
|
||||
CS = ui_state.sm["carState"]
|
||||
if not CS.standstill and self._prev_standstill:
|
||||
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
|
||||
if not gui_app.sunnypilot_ui() or self._should_auto_scroll_to_onroad():
|
||||
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
|
||||
self._prev_standstill = CS.standstill
|
||||
|
||||
def _on_interactive_timeout(self):
|
||||
|
||||
@@ -42,6 +42,8 @@ class TogglesLayoutMici(NavScroller):
|
||||
super().__init__()
|
||||
|
||||
self._personality_toggle = BigMultiParamToggle("driving personality", "LongitudinalPersonality", ["aggressive", "standard", "relaxed"])
|
||||
self._accel_controller_enabled = BigParamControl("enable accel controller", "AccelPersonalityEnabled")
|
||||
self._accel_personality_toggle = BigMultiParamToggle("acceleration profile", "AccelPersonality", ["eco", "normal", "sport"])
|
||||
self._experimental_btn = BigToggle("experimental mode", initial_state=ui_state.params.get_bool("ExperimentalMode"),
|
||||
toggle_callback=self._on_experimental_mode)
|
||||
is_metric_toggle = BigParamControl("use metric units", "IsMetric")
|
||||
@@ -53,6 +55,8 @@ class TogglesLayoutMici(NavScroller):
|
||||
|
||||
self._scroller.add_widgets([
|
||||
self._personality_toggle,
|
||||
self._accel_controller_enabled,
|
||||
self._accel_personality_toggle,
|
||||
self._experimental_btn,
|
||||
is_metric_toggle,
|
||||
ldw_toggle,
|
||||
@@ -65,6 +69,7 @@ class TogglesLayoutMici(NavScroller):
|
||||
# Toggle lists
|
||||
self._refresh_toggles = (
|
||||
("ExperimentalMode", self._experimental_btn),
|
||||
("AccelPersonalityEnabled", self._accel_controller_enabled),
|
||||
("IsMetric", is_metric_toggle),
|
||||
("IsLdwEnabled", ldw_toggle),
|
||||
("AlwaysOnDM", always_on_dm_toggle),
|
||||
@@ -104,17 +109,23 @@ class TogglesLayoutMici(NavScroller):
|
||||
if ui_state.has_longitudinal_control:
|
||||
self._experimental_btn.set_visible(True)
|
||||
self._personality_toggle.set_visible(True)
|
||||
self._accel_controller_enabled.set_visible(True)
|
||||
self._accel_personality_toggle.set_visible(True)
|
||||
else:
|
||||
# no long for now
|
||||
self._experimental_btn.set_visible(False)
|
||||
self._experimental_btn.set_checked(False)
|
||||
self._personality_toggle.set_visible(False)
|
||||
self._accel_controller_enabled.set_visible(False)
|
||||
self._accel_personality_toggle.set_visible(False)
|
||||
ui_state.params.remove("ExperimentalMode")
|
||||
|
||||
# Refresh toggles from params to mirror external changes
|
||||
for key, item in self._refresh_toggles:
|
||||
item.set_checked(ui_state.params.get_bool(key))
|
||||
|
||||
self._accel_personality_toggle.refresh()
|
||||
|
||||
def _on_experimental_mode(self, state: bool):
|
||||
if state and not ui_state.params.get_bool("ExperimentalModeConfirmed"):
|
||||
# Don't show enabled state until confirm
|
||||
|
||||
@@ -154,8 +154,8 @@ class ModelRenderer(Widget, ModelRendererSP):
|
||||
self._draw_lane_lines()
|
||||
self._draw_path(sm)
|
||||
|
||||
# if render_lead_indicator and radar_state:
|
||||
# self._draw_lead_indicator()
|
||||
if render_lead_indicator and radar_state:
|
||||
self._draw_lead_indicator()
|
||||
|
||||
def _update_raw_points(self, model):
|
||||
"""Update raw 3D points from model data"""
|
||||
|
||||
@@ -383,13 +383,18 @@ class BigMultiParamToggle(BigMultiToggle):
|
||||
self._load_value()
|
||||
|
||||
def _load_value(self):
|
||||
self.set_value(self._options[self._params.get(self._param) or 0])
|
||||
value = self._params.get(self._param, return_default=True)
|
||||
index = value if isinstance(value, int) else 0
|
||||
self.set_value(self._options[max(0, min(index, len(self._options) - 1))])
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
new_idx = self._options.index(self.value)
|
||||
self._params.put(self._param, new_idx)
|
||||
|
||||
def refresh(self):
|
||||
self._load_value()
|
||||
|
||||
|
||||
class BigParamControl(BigToggle):
|
||||
def __init__(self, text: str, param: str, toggle_callback: Callable | None = None):
|
||||
|
||||
@@ -143,7 +143,8 @@ class CruiseLayout(Widget):
|
||||
self.icbm_toggle.show_description(True)
|
||||
|
||||
if has_long or has_icbm:
|
||||
self.custom_acc_toggle.action_item.set_enabled(((has_long and not ui_state.CP.pcmCruise) or has_icbm) and ui_state.is_offroad())
|
||||
software_cruise_speed = has_long and (not ui_state.CP.pcmCruise or not ui_state.CP_SP.pcmCruiseSpeed)
|
||||
self.custom_acc_toggle.action_item.set_enabled((software_cruise_speed or has_icbm) and ui_state.is_offroad())
|
||||
self.dec_toggle.action_item.set_enabled(has_long)
|
||||
self.scc_v_toggle.action_item.set_enabled(True)
|
||||
self.scc_m_toggle.action_item.set_enabled(True)
|
||||
@@ -169,7 +170,7 @@ class CruiseLayout(Widget):
|
||||
show_custom_acc_desc = True
|
||||
else:
|
||||
if has_long or has_icbm:
|
||||
if has_long and ui_state.CP.pcmCruise:
|
||||
if has_long and ui_state.CP.pcmCruise and ui_state.CP_SP.pcmCruiseSpeed:
|
||||
new_custom_acc_desc = tr(ACC_PCMCRUISE_DISABLED_DESCRIPTION)
|
||||
show_custom_acc_desc = True
|
||||
else:
|
||||
|
||||
@@ -23,7 +23,7 @@ DESCRIPTIONS = {
|
||||
'stop_and_go_hack': tr_noop(
|
||||
'sunnypilot will allow some Toyota/Lexus cars to auto resume during stop and go traffic. ' +
|
||||
'This feature is only applicable to certain models that are able to use longitudinal control. This is an alpha feature. Use at your own risk.'
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.widgets.scroll_panel_sp import GuiScrollPanel2SP
|
||||
|
||||
|
||||
class MiciMainLayoutSP(MiciMainLayout):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
scroller = self._scroller
|
||||
scroller.scroll_panel = GuiScrollPanel2SP(scroller._horizontal, handle_out_of_bounds=not scroller._snap_items)
|
||||
|
||||
def _should_auto_scroll_to_onroad(self) -> bool:
|
||||
return not self._onroad_layout.is_on_info_panel()
|
||||
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from collections.abc import Callable
|
||||
import pyray as rl
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.widgets.scroller_sp import ScrollerSP
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.onroad.augmented_road_view import AugmentedRoadViewSP
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onroad_info_panel import OnroadInfoPanel
|
||||
|
||||
CONFIDENCE_BALL_VISIBLE_RATIO = 0.4
|
||||
HORIZONTAL_SETTLE_PX = 5
|
||||
HORIZONTAL_RESET_RATIO = 0.5
|
||||
|
||||
|
||||
class OnroadViewContainerSP(ScrollerSP):
|
||||
def __init__(self, bookmark_callback=None):
|
||||
super().__init__(horizontal=False, snap_items=True, spacing=0, pad=0, scroll_indicator=False, edge_shadows=False)
|
||||
self.road_view = AugmentedRoadViewSP(bookmark_callback=bookmark_callback)
|
||||
self.onroad_info_panel = OnroadInfoPanel(bookmark_callback=bookmark_callback)
|
||||
|
||||
self._scroller.add_widgets([
|
||||
self.road_view,
|
||||
self.onroad_info_panel,
|
||||
])
|
||||
self._scroller.set_reset_scroll_at_show(False)
|
||||
self._scroller.set_scrolling_enabled(lambda: abs(self.rect.x) < HORIZONTAL_SETTLE_PX)
|
||||
|
||||
for child in (self.road_view, self.onroad_info_panel):
|
||||
inner_touch_valid = child._touch_valid_callback
|
||||
child.set_touch_valid_callback(
|
||||
lambda inner=inner_touch_valid: self._touch_valid() and (inner() if inner else True)
|
||||
)
|
||||
|
||||
def set_rect(self, rect: rl.Rectangle):
|
||||
super().set_rect(rect)
|
||||
self.road_view.set_rect(rect)
|
||||
self.onroad_info_panel.set_rect(rect)
|
||||
return self
|
||||
|
||||
def is_swiping_left(self) -> bool:
|
||||
return self.road_view.is_swiping_left() or self.onroad_info_panel.is_swiping_left()
|
||||
|
||||
def set_click_callback(self, click_callback: Callable[[], None] | None) -> None:
|
||||
self.road_view.set_click_callback(click_callback)
|
||||
self.onroad_info_panel.set_click_callback(click_callback)
|
||||
|
||||
def is_on_info_panel(self) -> bool:
|
||||
"""True when scrolled past halfway toward onroad_info_panel (used by main layout
|
||||
to skip auto-pop-back-to-camera while user is reading the info panel)."""
|
||||
return abs(self._scroller.scroll_panel.get_offset()) > self._rect.height / 2
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
if abs(self.rect.x) > gui_app.width * HORIZONTAL_RESET_RATIO:
|
||||
self._scroller.scroll_panel.set_offset(0)
|
||||
|
||||
vertical_offset = self._scroller.scroll_panel.get_offset()
|
||||
show_ball = abs(vertical_offset) < rect.height * CONFIDENCE_BALL_VISIBLE_RATIO
|
||||
self.road_view.set_show_confidence_ball(show_ball)
|
||||
|
||||
super()._render(rect)
|
||||
@@ -0,0 +1,403 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
import pyray as rl
|
||||
from dataclasses import dataclass, field
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.selfdrive.ui.mici.onroad.alert_renderer import AlertRenderer
|
||||
from openpilot.selfdrive.ui.mici.onroad.augmented_road_view import BookmarkIcon
|
||||
|
||||
METER_TO_KM = 0.001
|
||||
METER_TO_MILE = 0.000621371
|
||||
|
||||
CONTENT_MARGIN = 16
|
||||
SPEED_LIMIT_SIGN_WIDTH = 146
|
||||
VIENNA_SIGN_SIZE = 146
|
||||
MUTCD_SIGN_HEIGHT = 178
|
||||
OFFSET_BADGE_SIZE = 50
|
||||
OFFSET_BADGE_PANEL_PADDING = 4
|
||||
MUTCD_OFFSET_SIGN_Y_SHIFT = 6
|
||||
VIENNA_BADGE_X_RATIO = 0.80
|
||||
VIENNA_BADGE_UPCOMING_X_RATIO = 0.70
|
||||
VIENNA_BADGE_Y_RATIO = -0.82
|
||||
UPCOMING_SIGN_SIZE_RATIO = 0.76
|
||||
UPCOMING_SIGN_OVERLAP_RATIO = 0.05
|
||||
UNIT_FONT_SIZE = 40
|
||||
SPEED_FONT_SIZE = 114
|
||||
ROAD_FONT_SIZE = 32
|
||||
SCC_TAG_WIDTH = 78
|
||||
SCC_TAG_HEIGHT = 30
|
||||
SCC_TAG_GAP = 5
|
||||
COLUMN_GAP = 12
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OnroadInfoPanelColors:
|
||||
white: rl.Color = rl.WHITE
|
||||
black: rl.Color = rl.BLACK
|
||||
red: rl.Color = field(default_factory=lambda: rl.Color(255, 0, 0, 255))
|
||||
green: rl.Color = field(default_factory=lambda: rl.Color(0, 255, 0, 255))
|
||||
grey: rl.Color = field(default_factory=lambda: rl.Color(190, 195, 190, 255))
|
||||
light_grey: rl.Color = field(default_factory=lambda: rl.Color(200, 200, 200, 255))
|
||||
dark_grey: rl.Color = field(default_factory=lambda: rl.Color(100, 100, 100, 255))
|
||||
bg_dark: rl.Color = field(default_factory=lambda: rl.Color(0, 0, 0, 255))
|
||||
card_bg: rl.Color = field(default_factory=lambda: rl.Color(50, 50, 50, 200))
|
||||
badge_bg: rl.Color = field(default_factory=lambda: rl.Color(60, 60, 60, 255))
|
||||
|
||||
|
||||
COLORS = OnroadInfoPanelColors()
|
||||
|
||||
|
||||
class OnroadInfoPanel(Widget):
|
||||
def __init__(self, bookmark_callback=None):
|
||||
super().__init__()
|
||||
self.speed_limit: float = 0.0
|
||||
self.speed_limit_valid: bool = False
|
||||
self.speed_limit_offset: float = 0.0
|
||||
self.next_speed_limit: float = 0.0
|
||||
self.next_speed_limit_distance: float = 0.0
|
||||
self.road_name: str = ""
|
||||
self.current_speed: float = 0.0
|
||||
self.set_speed: float = 0.0
|
||||
self.cruise_enabled: bool = False
|
||||
|
||||
self._sign_slide: float = 0.0
|
||||
|
||||
self._font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
|
||||
self._font_semi_bold: rl.Font = gui_app.font(FontWeight.SEMI_BOLD)
|
||||
self._font_medium: rl.Font = gui_app.font(FontWeight.MEDIUM)
|
||||
|
||||
self._marquee_offset: float = 0.0
|
||||
self._marquee_direction: int = 1
|
||||
self._marquee_pause_timer: float = 0.0
|
||||
self._marquee_speed: float = 40.0
|
||||
self._marquee_pause_duration: float = 1.5
|
||||
|
||||
self._alert_renderer = AlertRenderer()
|
||||
self._alert_alpha_filter = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps)
|
||||
|
||||
self._bookmark_icon = BookmarkIcon(bookmark_callback)
|
||||
|
||||
def is_swiping_left(self) -> bool:
|
||||
return self._bookmark_icon.is_swiping_left()
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos) -> None:
|
||||
# Mirror stock AugmentedRoadView: suppress click while bookmark gesture active
|
||||
if not self._bookmark_icon.interacting():
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
|
||||
def _update_state(self) -> None:
|
||||
sm = ui_state.sm
|
||||
speed_conv = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
|
||||
|
||||
if sm.valid["longitudinalPlanSP"]:
|
||||
lp_sp = sm["longitudinalPlanSP"]
|
||||
resolver = lp_sp.speedLimit.resolver
|
||||
self.speed_limit = resolver.speedLimit * speed_conv
|
||||
self.speed_limit_valid = resolver.speedLimitValid
|
||||
self.speed_limit_offset = resolver.speedLimitOffset * speed_conv
|
||||
|
||||
if sm.valid["liveMapDataSP"]:
|
||||
lmd = sm["liveMapDataSP"]
|
||||
self.next_speed_limit = lmd.speedLimitAhead * speed_conv
|
||||
self.next_speed_limit_distance = lmd.speedLimitAheadDistance
|
||||
self.road_name = lmd.roadName
|
||||
|
||||
if sm.updated["carState"]:
|
||||
self.current_speed = sm["carState"].vEgo * speed_conv
|
||||
|
||||
if sm.valid["carState"] and sm.valid["controlsState"]:
|
||||
self.cruise_enabled = sm["carState"].cruiseState.enabled
|
||||
v_cruise_cluster = sm["carState"].vCruiseCluster
|
||||
set_speed_kph = sm["controlsState"].vCruiseDEPRECATED if v_cruise_cluster == 0.0 else v_cruise_cluster
|
||||
self.set_speed = set_speed_kph * (METER_TO_MILE / METER_TO_KM) if not ui_state.is_metric else set_speed_kph
|
||||
|
||||
def _render(self, rect: rl.Rectangle) -> None:
|
||||
self._update_state()
|
||||
|
||||
rl.draw_rectangle(int(rect.x), int(rect.y), int(rect.width), int(rect.height), COLORS.bg_dark)
|
||||
|
||||
left_x = rect.x + CONTENT_MARGIN
|
||||
|
||||
if self.cruise_enabled:
|
||||
unit = tr("MAX")
|
||||
display_speed = self.set_speed
|
||||
else:
|
||||
unit = tr("km/h") if ui_state.is_metric else tr("MPH")
|
||||
display_speed = self.current_speed
|
||||
|
||||
display_speed_text = str(round(display_speed))
|
||||
if self.speed_limit_valid and display_speed > self.speed_limit:
|
||||
speed_color = COLORS.red
|
||||
else:
|
||||
speed_color = COLORS.white
|
||||
|
||||
sign_width = min(SPEED_LIMIT_SIGN_WIDTH, rect.width * 0.30)
|
||||
sign_height = VIENNA_SIGN_SIZE if ui_state.is_metric else MUTCD_SIGN_HEIGHT
|
||||
|
||||
has_upcoming_limit = self.next_speed_limit > 0 and self.next_speed_limit != self.speed_limit
|
||||
target_sign_slide = 1.0 if has_upcoming_limit else 0.0
|
||||
slide_speed = 3.0 * rl.get_frame_time()
|
||||
if self._sign_slide < target_sign_slide:
|
||||
self._sign_slide = min(self._sign_slide + slide_speed, target_sign_slide)
|
||||
elif self._sign_slide > target_sign_slide:
|
||||
self._sign_slide = max(self._sign_slide - slide_speed, target_sign_slide)
|
||||
|
||||
upcoming_width = int(sign_width * UPCOMING_SIGN_SIZE_RATIO)
|
||||
upcoming_height = int(sign_height * UPCOMING_SIGN_SIZE_RATIO)
|
||||
upcoming_reserved_width = int(upcoming_width * 0.85) + 5
|
||||
sign_x_without_upcoming = rect.x + rect.width - sign_width - CONTENT_MARGIN
|
||||
sign_x_with_upcoming = rect.x + rect.width - sign_width - CONTENT_MARGIN - upcoming_reserved_width
|
||||
sign_x = sign_x_without_upcoming + (sign_x_with_upcoming - sign_x_without_upcoming) * self._sign_slide
|
||||
sign_y = rect.y + (rect.height - sign_height) / 2
|
||||
if not ui_state.is_metric and self.speed_limit_offset != 0 and self.speed_limit_valid:
|
||||
sign_y += MUTCD_OFFSET_SIGN_Y_SHIFT
|
||||
|
||||
readout_right = sign_x - COLUMN_GAP
|
||||
readout_width = max(1, readout_right - left_x)
|
||||
road_y = rect.y + rect.height - 44
|
||||
|
||||
unit_font_size = self._fit_font_size(self._font_semi_bold, unit, readout_width, 46, UNIT_FONT_SIZE, 28)
|
||||
speed_font_size = self._fit_font_size(self._font_bold, display_speed_text, readout_width, road_y - (rect.y + 54) - 8,
|
||||
SPEED_FONT_SIZE, 76)
|
||||
speed_size = measure_text_cached(self._font_bold, display_speed_text, speed_font_size)
|
||||
speed_y = min(rect.y + 54, road_y - speed_size.y - 8)
|
||||
unit_y = max(rect.y + 14, speed_y - unit_font_size - 6)
|
||||
|
||||
rl.draw_text_ex(self._font_semi_bold, unit, rl.Vector2(left_x, unit_y), unit_font_size, 0, COLORS.grey)
|
||||
rl.draw_text_ex(self._font_bold, display_speed_text, rl.Vector2(left_x, speed_y), speed_font_size, 0, speed_color)
|
||||
self._draw_road_name(left_x, road_y, readout_width)
|
||||
|
||||
if has_upcoming_limit and self._sign_slide > 0.01:
|
||||
upcoming_speed_text = str(round(self.next_speed_limit))
|
||||
distance_text = self._format_distance(self.next_speed_limit_distance)
|
||||
upcoming_x = sign_x + sign_width - int(upcoming_width * UPCOMING_SIGN_OVERLAP_RATIO)
|
||||
upcoming_y = sign_y + (sign_height - upcoming_height) / 2
|
||||
|
||||
upcoming_speed_color = COLORS.black
|
||||
if ui_state.is_metric:
|
||||
self._draw_vienna_sign(upcoming_x, upcoming_y, upcoming_width, upcoming_height, upcoming_speed_text, upcoming_speed_color, is_upcoming=True)
|
||||
else:
|
||||
self._draw_mutcd_sign(upcoming_x, upcoming_y, upcoming_width, upcoming_height, upcoming_speed_text, upcoming_speed_color, is_upcoming=True)
|
||||
|
||||
distance_font_size = self._fit_font_size(self._font_medium, distance_text, upcoming_width, 30, 24, 16)
|
||||
distance_size = measure_text_cached(self._font_medium, distance_text, distance_font_size)
|
||||
rl.draw_text_ex(self._font_medium, distance_text, rl.Vector2(upcoming_x + upcoming_width / 2 - distance_size.x / 2, upcoming_y + upcoming_height),
|
||||
distance_font_size, 0, COLORS.grey)
|
||||
|
||||
self._draw_speed_limit_sign(sign_x, sign_y, sign_width, sign_height)
|
||||
|
||||
if self.speed_limit_offset != 0 and self.speed_limit_valid:
|
||||
offset_text = str(abs(round(self.speed_limit_offset)))
|
||||
badge_size = OFFSET_BADGE_SIZE
|
||||
badge_rect = self._offset_badge_rect(rect, sign_x, sign_y, sign_width, sign_height, badge_size, has_upcoming_limit)
|
||||
|
||||
if ui_state.is_metric:
|
||||
badge_radius = badge_size / 2
|
||||
badge_center_x = badge_rect.x + badge_radius
|
||||
badge_center_y = badge_rect.y + badge_radius
|
||||
rl.draw_circle(int(badge_center_x), int(badge_center_y), badge_radius + 2, COLORS.dark_grey)
|
||||
rl.draw_circle(int(badge_center_x), int(badge_center_y), badge_radius, COLORS.badge_bg)
|
||||
self._draw_text_centered_fit(self._font_bold, offset_text, 32, rl.Vector2(badge_center_x, badge_center_y), COLORS.white,
|
||||
badge_size - 10, badge_size - 8, min_size=24)
|
||||
else:
|
||||
rl.draw_rectangle_rounded(badge_rect, 0.25, 10, COLORS.badge_bg)
|
||||
rl.draw_rectangle_rounded_lines_ex(badge_rect, 0.25, 10, 2, COLORS.dark_grey)
|
||||
self._draw_text_centered_fit(self._font_bold, offset_text, 32, rl.Vector2(badge_rect.x + badge_size / 2, badge_rect.y + badge_size / 2),
|
||||
COLORS.white, badge_size - 10, badge_size - 8, min_size=24)
|
||||
|
||||
scc_tag_x = min(left_x + speed_size.x + COLUMN_GAP, readout_right - SCC_TAG_WIDTH)
|
||||
scc_tag_y = speed_y + (speed_size.y - (SCC_TAG_HEIGHT * 2 + SCC_TAG_GAP)) / 2
|
||||
if scc_tag_x >= left_x + speed_size.x + 8:
|
||||
self._draw_scc_icons(scc_tag_x, scc_tag_y, readout_right)
|
||||
|
||||
self._bookmark_icon.render(rect)
|
||||
|
||||
if ui_state.started:
|
||||
alert_obj, no_alert = self._alert_renderer.will_render()
|
||||
self._alert_alpha_filter.update(0 if no_alert else 1)
|
||||
alpha = self._alert_alpha_filter.x
|
||||
if alpha > 0.01:
|
||||
rl.draw_rectangle(int(rect.x), int(rect.y), int(rect.width), int(rect.height), rl.Color(0, 0, 0, int(150 * alpha)))
|
||||
self._alert_renderer.render(rect)
|
||||
|
||||
def _draw_scc_icons(self, x: float, y: float, right_limit: float) -> None:
|
||||
sm = ui_state.sm
|
||||
if not sm.valid["longitudinalPlanSP"]:
|
||||
return
|
||||
scc = sm["longitudinalPlanSP"].smartCruiseControl
|
||||
|
||||
drawn = 0
|
||||
|
||||
for label, active in [("SCC-V", scc.vision.active), ("SCC-M", scc.map.active)]:
|
||||
if not active:
|
||||
continue
|
||||
tag_x = x
|
||||
if tag_x + SCC_TAG_WIDTH > right_limit:
|
||||
return
|
||||
tag_y = y + drawn * (SCC_TAG_HEIGHT + SCC_TAG_GAP)
|
||||
rl.draw_rectangle_rounded(rl.Rectangle(tag_x, tag_y, SCC_TAG_WIDTH, SCC_TAG_HEIGHT), 0.3, 10, COLORS.green)
|
||||
self._draw_text_centered_fit(self._font_bold, label, 18, rl.Vector2(tag_x + SCC_TAG_WIDTH / 2, tag_y + SCC_TAG_HEIGHT / 2), COLORS.black,
|
||||
SCC_TAG_WIDTH - 10, SCC_TAG_HEIGHT - 4, min_size=14)
|
||||
drawn += 1
|
||||
|
||||
def _draw_speed_limit_sign(self, x: float, y: float, sign_width: float, sign_height: float) -> None:
|
||||
speed_str = str(round(self.speed_limit)) if self.speed_limit_valid and self.speed_limit > 0 else "--"
|
||||
speed_color = COLORS.black if not self.speed_limit_valid or self.current_speed <= self.speed_limit else COLORS.red
|
||||
|
||||
if ui_state.is_metric:
|
||||
self._draw_vienna_sign(x, y, sign_width, sign_height, speed_str, speed_color, is_upcoming=False)
|
||||
else:
|
||||
self._draw_mutcd_sign(x, y, sign_width, sign_height, speed_str, speed_color, is_upcoming=False)
|
||||
|
||||
def _draw_road_name(self, x: float, y: float, width: float) -> None:
|
||||
if width <= 0:
|
||||
return
|
||||
|
||||
road_display = self.road_name if self.road_name else "--"
|
||||
font_size = self._fit_font_size(self._font_semi_bold, road_display, width, 38, ROAD_FONT_SIZE, 28)
|
||||
road_size = measure_text_cached(self._font_semi_bold, road_display, font_size)
|
||||
text_width = road_size.x
|
||||
|
||||
if text_width <= width:
|
||||
self._marquee_offset = 0.0
|
||||
self._marquee_direction = 1
|
||||
self._marquee_pause_timer = 0.0
|
||||
rl.draw_text_ex(self._font_semi_bold, road_display, rl.Vector2(x, y), font_size, 0, COLORS.white)
|
||||
else:
|
||||
overflow = text_width - width
|
||||
dt = rl.get_frame_time()
|
||||
|
||||
if self._marquee_pause_timer > 0:
|
||||
self._marquee_pause_timer -= dt
|
||||
else:
|
||||
self._marquee_offset += self._marquee_direction * self._marquee_speed * dt
|
||||
|
||||
if self._marquee_offset >= overflow:
|
||||
self._marquee_offset = overflow
|
||||
self._marquee_direction = -1
|
||||
self._marquee_pause_timer = self._marquee_pause_duration
|
||||
elif self._marquee_offset <= 0:
|
||||
self._marquee_offset = 0
|
||||
self._marquee_direction = 1
|
||||
self._marquee_pause_timer = self._marquee_pause_duration
|
||||
|
||||
rl.begin_scissor_mode(int(x), int(y), int(width), int(road_size.y + 4))
|
||||
text_pos = rl.Vector2(x - self._marquee_offset, y)
|
||||
rl.draw_text_ex(self._font_semi_bold, road_display, text_pos, font_size, 0, COLORS.white)
|
||||
rl.end_scissor_mode()
|
||||
|
||||
def _draw_vienna_sign(self, x: float, y: float, width: float, height: float, speed_str: str, speed_color: rl.Color, is_upcoming: bool = False) -> None:
|
||||
center = rl.Vector2(x + width / 2, y + height / 2)
|
||||
outer_radius = min(width, height) / 2
|
||||
|
||||
rl.draw_circle_v(center, outer_radius, COLORS.white)
|
||||
ring_width = outer_radius * 0.18
|
||||
rl.draw_ring(center, outer_radius - ring_width, outer_radius, 0, 360, 36, COLORS.red)
|
||||
|
||||
font_size = outer_radius * (0.7 if len(speed_str) >= 3 else 0.9)
|
||||
self._draw_text_centered_fit(self._font_bold, speed_str, int(font_size), center, speed_color, width * 0.72, height * 0.50, min_size=24)
|
||||
|
||||
def _draw_mutcd_sign(self, x: float, y: float, width: float, height: float, speed_str: str, speed_color: rl.Color, is_upcoming: bool = False) -> None:
|
||||
sign_rect = rl.Rectangle(x, y, width, height)
|
||||
rl.draw_rectangle_rounded(sign_rect, 0.35, 10, COLORS.white)
|
||||
|
||||
inset = max(4, width * 0.05)
|
||||
inner_rect = rl.Rectangle(x + inset, y + inset, width - inset * 2, height - inset * 2)
|
||||
outer_radius = 0.35 * width / 2.0
|
||||
inner_radius = outer_radius - inset
|
||||
inner_roundness = inner_radius / (inner_rect.width / 2.0)
|
||||
rl.draw_rectangle_rounded_lines_ex(inner_rect, inner_roundness, 10, 3, COLORS.black)
|
||||
|
||||
mid_x = x + width / 2
|
||||
label_size = max(18, int(width * 0.26))
|
||||
if is_upcoming:
|
||||
self._draw_text_centered_fit(self._font_bold, tr("AHEAD"), int(width * 0.34), rl.Vector2(mid_x, y + height * 0.28), COLORS.black,
|
||||
width * 0.94, height * 0.32, min_size=20)
|
||||
else:
|
||||
self._draw_text_centered_fit(self._font_bold, tr("SPEED"), label_size, rl.Vector2(mid_x, y + height * 0.20), COLORS.black,
|
||||
width * 0.84, height * 0.24, min_size=16)
|
||||
self._draw_text_centered_fit(self._font_bold, tr("LIMIT"), label_size, rl.Vector2(mid_x, y + height * 0.40), COLORS.black,
|
||||
width * 0.84, height * 0.24, min_size=16)
|
||||
|
||||
speed_font_size = int(width * 0.60) if len(speed_str) >= 3 else int(width * 0.72)
|
||||
self._draw_text_centered_fit(self._font_bold, speed_str, speed_font_size, rl.Vector2(mid_x, y + height * 0.72), speed_color,
|
||||
width * 0.90, height * 0.52, min_size=32)
|
||||
|
||||
def _draw_text_centered(self, font, text, size, pos_center, color):
|
||||
sz = measure_text_cached(font, text, size)
|
||||
rl.draw_text_ex(font, text, rl.Vector2(pos_center.x - sz.x / 2, pos_center.y - sz.y / 2), size, 0, color)
|
||||
|
||||
def _draw_text_centered_fit(self, font, text, size, pos_center, color, max_width: float, max_height: float, min_size: int = 10):
|
||||
size = self._fit_font_size(font, text, max_width, max_height, size, min_size)
|
||||
self._draw_text_centered(font, text, size, pos_center, color)
|
||||
|
||||
def _fit_font_size(self, font, text: str, max_width: float, max_height: float, max_size: int | float, min_size: int) -> int:
|
||||
size = int(max_size)
|
||||
while size > min_size:
|
||||
text_size = measure_text_cached(font, text, size)
|
||||
if text_size.x <= max_width and text_size.y <= max_height:
|
||||
return size
|
||||
size -= 2
|
||||
return min_size
|
||||
|
||||
def _offset_badge_rect(self, panel_rect: rl.Rectangle, sign_x: float, sign_y: float, sign_width: float, sign_height: float,
|
||||
badge_size: float, has_upcoming_limit: bool) -> rl.Rectangle:
|
||||
if ui_state.is_metric:
|
||||
radius = min(sign_width, sign_height) / 2
|
||||
center_x = sign_x + sign_width / 2
|
||||
center_y = sign_y + sign_height / 2
|
||||
badge_x_ratio = VIENNA_BADGE_UPCOMING_X_RATIO if has_upcoming_limit else VIENNA_BADGE_X_RATIO
|
||||
badge_center_x = center_x + radius * badge_x_ratio
|
||||
badge_center_y = center_y + radius * VIENNA_BADGE_Y_RATIO
|
||||
badge_x = badge_center_x - badge_size / 2
|
||||
badge_y = badge_center_y - badge_size / 2
|
||||
else:
|
||||
badge_x = sign_x + sign_width - badge_size * 0.45
|
||||
badge_y = sign_y - badge_size * 0.75
|
||||
|
||||
return rl.Rectangle(
|
||||
self._clamp(
|
||||
badge_x,
|
||||
panel_rect.x + OFFSET_BADGE_PANEL_PADDING,
|
||||
panel_rect.x + panel_rect.width - badge_size - OFFSET_BADGE_PANEL_PADDING,
|
||||
),
|
||||
self._clamp(
|
||||
badge_y,
|
||||
panel_rect.y + OFFSET_BADGE_PANEL_PADDING,
|
||||
panel_rect.y + panel_rect.height - badge_size - OFFSET_BADGE_PANEL_PADDING,
|
||||
),
|
||||
badge_size,
|
||||
badge_size,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _clamp(value: float, min_value: float, max_value: float) -> float:
|
||||
return max(min_value, min(max_value, value))
|
||||
|
||||
def _format_distance(self, distance: float) -> str:
|
||||
if ui_state.is_metric:
|
||||
if distance < 50:
|
||||
return tr("Near")
|
||||
if distance >= 1000:
|
||||
return f"{distance * METER_TO_KM:.1f}" + tr("km")
|
||||
if distance < 200:
|
||||
rounded = max(10, int(distance / 10) * 10)
|
||||
else:
|
||||
rounded = int(distance / 100) * 100
|
||||
return str(rounded) + tr("m")
|
||||
else:
|
||||
distance_mi = distance * METER_TO_MILE
|
||||
if distance_mi < 0.1:
|
||||
return tr("Near")
|
||||
return f"{distance_mi:.1f}" + tr("mi")
|
||||
@@ -0,0 +1,29 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
from openpilot.selfdrive.ui.mici.onroad.augmented_road_view import AugmentedRoadView
|
||||
|
||||
|
||||
class _SuppressedConfidenceBall:
|
||||
def render(self, *_):
|
||||
pass
|
||||
|
||||
|
||||
class AugmentedRoadViewSP(AugmentedRoadView):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._show_confidence_ball: bool = True
|
||||
self._real_confidence_ball = self._confidence_ball
|
||||
self._confidence_ball = _SuppressedConfidenceBall()
|
||||
|
||||
def set_show_confidence_ball(self, show: bool) -> None:
|
||||
self._show_confidence_ball = show
|
||||
|
||||
def _render(self, _) -> None:
|
||||
super()._render(_)
|
||||
if self._show_confidence_ball:
|
||||
self._real_confidence_ball.render(self.rect)
|
||||
@@ -0,0 +1,83 @@
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.system.ui.lib.application import MouseEvent, MousePos, gui_app
|
||||
from openpilot.system.ui.lib.scroll_panel2 import ScrollState
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets import scroller as scroller_mod
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.widgets.scroll_panel_sp import GuiScrollPanel2SP
|
||||
|
||||
|
||||
class DummyScrollIndicator:
|
||||
def update(self, *_) -> None:
|
||||
pass
|
||||
|
||||
def render(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class DummyWidget(Widget):
|
||||
def __init__(self, rect: rl.Rectangle):
|
||||
super().__init__()
|
||||
self.set_rect(rect)
|
||||
|
||||
def _render(self, _) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def _mouse_event(x: float, y: float, *, pressed: bool = False, released: bool = False,
|
||||
down: bool = True, t: float = 0.0) -> MouseEvent:
|
||||
return MouseEvent(MousePos(x, y), 0, pressed, released, down, t)
|
||||
|
||||
|
||||
class TestScrollerSP(OpenpilotTestCase):
|
||||
def test_vertical_snap_items_are_supported(self, monkeypatch):
|
||||
monkeypatch.setattr(scroller_mod, "ScrollIndicator", DummyScrollIndicator)
|
||||
|
||||
scroller = scroller_mod._Scroller([], horizontal=False, snap_items=True, scroll_indicator=False)
|
||||
scroller.set_rect(rl.Rectangle(0, 0, 100, 100))
|
||||
scroller.scroll_panel.set_offset(-60)
|
||||
|
||||
captured_snap_target = None
|
||||
|
||||
def update(_, __, snap_target=None):
|
||||
nonlocal captured_snap_target
|
||||
captured_snap_target = snap_target
|
||||
return scroller.scroll_panel.get_offset()
|
||||
|
||||
monkeypatch.setattr(scroller.scroll_panel, "update", update)
|
||||
|
||||
visible_items: list[Widget] = [
|
||||
DummyWidget(rl.Rectangle(0, -60, 100, 100)),
|
||||
DummyWidget(rl.Rectangle(0, 40, 100, 100)),
|
||||
]
|
||||
scroller._get_scroll(visible_items, 200)
|
||||
|
||||
assert captured_snap_target == -100
|
||||
|
||||
def test_scroll_panel_sp_rejects_orthogonal_drags(self, monkeypatch):
|
||||
panel = GuiScrollPanel2SP(horizontal=True)
|
||||
bounds = rl.Rectangle(0, 0, 100, 100)
|
||||
|
||||
monkeypatch.setattr(gui_app, "_mouse_events", [_mouse_event(10, 10, pressed=True, t=1.0)])
|
||||
panel.update(bounds, 200)
|
||||
assert panel.state == ScrollState.PRESSED
|
||||
|
||||
monkeypatch.setattr(gui_app, "_mouse_events", [_mouse_event(23, 60, t=1.1)])
|
||||
panel.update(bounds, 200)
|
||||
|
||||
assert panel.state == ScrollState.STEADY
|
||||
assert panel.get_offset() == 0
|
||||
|
||||
def test_scroll_panel_sp_can_disable_out_of_bounds_handling(self, monkeypatch):
|
||||
panel = GuiScrollPanel2SP(horizontal=False, handle_out_of_bounds=False)
|
||||
bounds = rl.Rectangle(0, 0, 100, 100)
|
||||
monkeypatch.setattr(gui_app, "_mouse_events", [])
|
||||
|
||||
panel.set_offset(20)
|
||||
panel.update(bounds, 200)
|
||||
assert panel.get_offset() == 0
|
||||
|
||||
panel.set_offset(-150)
|
||||
panel.update(bounds, 200)
|
||||
assert panel.get_offset() == -100
|
||||
@@ -0,0 +1,33 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
import pyray as rl
|
||||
from openpilot.system.ui.lib.application import MouseEvent
|
||||
from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2, ScrollState
|
||||
|
||||
|
||||
class GuiScrollPanel2SP(GuiScrollPanel2):
|
||||
"""Scroll panel behavior for nested Mici pagers."""
|
||||
|
||||
def __init__(self, horizontal: bool = True, handle_out_of_bounds: bool = True) -> None:
|
||||
super().__init__(horizontal, handle_out_of_bounds=handle_out_of_bounds)
|
||||
|
||||
def _handle_mouse_event(self, mouse_event: MouseEvent, bounds: rl.Rectangle, bounds_size: float,
|
||||
content_size: float) -> None:
|
||||
state_before_update = self._state
|
||||
super()._handle_mouse_event(mouse_event, bounds, bounds_size, content_size)
|
||||
|
||||
if self._state == ScrollState.MANUAL_SCROLL and state_before_update == ScrollState.PRESSED and \
|
||||
self._initial_click_event is not None:
|
||||
drag_x = abs(mouse_event.pos.x - self._initial_click_event.pos.x)
|
||||
drag_y = abs(mouse_event.pos.y - self._initial_click_event.pos.y)
|
||||
primary_drag = drag_x if self._horizontal else drag_y
|
||||
cross_drag = drag_y if self._horizontal else drag_x
|
||||
if cross_drag > primary_drag:
|
||||
self._state = ScrollState.STEADY
|
||||
self._velocity = 0.0
|
||||
self._velocity_buffer.clear()
|
||||
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.widgets.scroll_panel_sp import GuiScrollPanel2SP
|
||||
|
||||
|
||||
class ScrollerSP(Scroller):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
inner = self._scroller
|
||||
inner.scroll_panel = GuiScrollPanel2SP(inner._horizontal, handle_out_of_bounds=not inner._snap_items)
|
||||
@@ -10,6 +10,9 @@ from openpilot.selfdrive.ui.layouts.main import MainLayout
|
||||
from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
|
||||
if gui_app.sunnypilot_ui():
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.main import MiciMainLayoutSP as MiciMainLayout
|
||||
|
||||
BIG_UI = gui_app.big_ui()
|
||||
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
#define SUNNYPILOT_VERSION "2026.09.16-4895"
|
||||
#define SUNNYPILOT_VERSION "2026.10.10-4923"
|
||||
|
||||
+1
-1
@@ -115,7 +115,7 @@ class IntelligentCruiseButtonManagement:
|
||||
self.is_ready = ready and not button_pressed
|
||||
|
||||
def run(self, CS: car.CarState, CC: car.CarControl, LP_SP: custom.LongitudinalPlanSP, is_metric: bool) -> None:
|
||||
if self.CP_SP.pcmCruiseSpeed:
|
||||
if self.CP_SP.pcmCruiseSpeed or not self.CP_SP.intelligentCruiseButtonManagementAvailable:
|
||||
return
|
||||
|
||||
self.is_metric = is_metric
|
||||
|
||||
@@ -136,6 +136,10 @@ def initialize_params(params) -> list[dict[str, Any]]:
|
||||
keys.extend([
|
||||
"ToyotaEnforceStockLongitudinal",
|
||||
"ToyotaStopAndGoHack",
|
||||
"ToyotaTSS2Long",
|
||||
"ToyotaEnhancedBsm",
|
||||
"ToyotaAutoHold",
|
||||
"ToyotaBrakeOnset",
|
||||
])
|
||||
|
||||
return [{k: params.get(k, return_default=True)} for k in keys]
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
from opendbc.can.parser import CANParser
|
||||
from opendbc.car import create_button_events
|
||||
from opendbc.car.structs import car
|
||||
from opendbc.car.toyota.carstate import get_virtual_cruise_button, VIRTUAL_CRUISE_BUTTONS
|
||||
from openpilot.cereal import custom
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.common.parameterized import parameterized, parameterized_class
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.car.cruise import V_CRUISE_INITIAL
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.selfdrive.car.cruise import TOYOTA_VIRTUAL_CRUISE_LONG_PRESS, VCruiseHelper, V_CRUISE_INITIAL, V_CRUISE_UNSET
|
||||
from openpilot.selfdrive.car.tests.test_cruise_speed import TestVCruiseHelper
|
||||
from openpilot.sunnypilot.selfdrive.car.interfaces import initialize_params
|
||||
|
||||
ButtonEvent = car.CarState.ButtonEvent
|
||||
ButtonType = car.CarState.ButtonEvent.Type
|
||||
|
||||
|
||||
class TestToyotaParamsHandoff(OpenpilotTestCase):
|
||||
def test_tss2_long_tuning_param_is_forwarded_to_opendbc(self):
|
||||
keys = {next(iter(entry)) for entry in initialize_params(Params())}
|
||||
assert "ToyotaTSS2Long" in keys
|
||||
|
||||
|
||||
# TODO: test pcmCruise and pcmCruiseSpeed
|
||||
@parameterized_class(('pcm_cruise', 'pcm_cruise_speed'), [(False, True)])
|
||||
class TestCustomAccIncrements(TestVCruiseHelper):
|
||||
@@ -114,8 +126,8 @@ class TestCustomAccIncrements(TestVCruiseHelper):
|
||||
def test_rounding_behavior(self):
|
||||
"""Test rounding behavior for 5 and 10 increments"""
|
||||
test_cases = [
|
||||
(47, 5, 50), # 47 -> 50 (round up to next 5)
|
||||
(45, 5, 50), # 45 -> 50 (already at 5, increment by 5)
|
||||
(47, 5, 50), # 47 -> 50 (round up to next 5)
|
||||
(45, 5, 50), # 45 -> 50 (already at 5, increment by 5)
|
||||
(43, 10, 50), # 43 -> 50 (round up to next 10)
|
||||
(40, 10, 50), # 40 -> 50 (already at 10, increment by 10)
|
||||
]
|
||||
@@ -146,3 +158,302 @@ class TestCustomAccIncrements(TestVCruiseHelper):
|
||||
initial_speed = self.v_cruise_helper.v_cruise_kph
|
||||
self.press_button_long(ButtonType.accelCruise)
|
||||
assert self.v_cruise_helper.v_cruise_kph == initial_speed + 10 # Should fallback to 10
|
||||
|
||||
|
||||
class TestToyotaVirtualCruiseSpeed(OpenpilotTestCase):
|
||||
def setup_method(self):
|
||||
self.params = Params()
|
||||
self.params.put_bool("CustomAccIncrementsEnabled", True, block=True)
|
||||
self.params.put("CustomAccShortPressIncrement", 5, block=True)
|
||||
self.params.put("CustomAccLongPressIncrement", 5, block=True)
|
||||
|
||||
CP = car.CarParams(brand="toyota", pcmCruise=True, openpilotLongitudinalControl=True)
|
||||
CP_SP = custom.CarParamsSP(pcmCruiseSpeed=False)
|
||||
self.v_cruise_helper = VCruiseHelper(CP, CP_SP)
|
||||
self.v_cruise_helper.read_custom_set_speed_params()
|
||||
self.route_parser = CANParser("toyota_nodsu_pt_generated", [("CLUTCH", 16)], 0)
|
||||
self.route_button = 0
|
||||
|
||||
@staticmethod
|
||||
def car_state(canonical_kph, cluster_kph, *, available=True, standstill=False, gas_pressed=False, v_ego_kph=0.0, button_events=None):
|
||||
CS = car.CarState(
|
||||
gasPressed=gas_pressed,
|
||||
vEgo=v_ego_kph * CV.KPH_TO_MS,
|
||||
cruiseState={
|
||||
"available": available,
|
||||
"speed": canonical_kph * CV.KPH_TO_MS,
|
||||
"speedCluster": cluster_kph * CV.KPH_TO_MS,
|
||||
"standstill": standstill,
|
||||
},
|
||||
)
|
||||
CS.buttonEvents = button_events or []
|
||||
return CS
|
||||
|
||||
def seed_enabled(self, canonical_kph, cluster_kph, *, is_metric=True):
|
||||
CS = self.car_state(canonical_kph, cluster_kph)
|
||||
self.v_cruise_helper.update_v_cruise(CS, enabled=False, is_metric=is_metric)
|
||||
self.v_cruise_helper.update_v_cruise(CS, enabled=True, is_metric=is_metric)
|
||||
self.v_cruise_helper.update_v_cruise(CS, enabled=True, is_metric=is_metric)
|
||||
assert self.v_cruise_helper.v_cruise_kph == canonical_kph
|
||||
assert self.v_cruise_helper.v_cruise_cluster_kph == cluster_kph
|
||||
|
||||
def press(self, button_type, canonical_kph, cluster_kph, hold_frames=0, *, standstill=False, gas_pressed=False, v_ego_kph=0.0, is_metric=True):
|
||||
pressed = [ButtonEvent(type=button_type, pressed=True)]
|
||||
self.v_cruise_helper.update_v_cruise(
|
||||
self.car_state(canonical_kph, cluster_kph, standstill=standstill, gas_pressed=gas_pressed, v_ego_kph=v_ego_kph, button_events=pressed),
|
||||
enabled=True,
|
||||
is_metric=is_metric,
|
||||
)
|
||||
for _ in range(hold_frames):
|
||||
self.v_cruise_helper.update_v_cruise(
|
||||
self.car_state(canonical_kph, cluster_kph, standstill=standstill, gas_pressed=gas_pressed, v_ego_kph=v_ego_kph),
|
||||
enabled=True,
|
||||
is_metric=is_metric,
|
||||
)
|
||||
released = [ButtonEvent(type=button_type, pressed=False)]
|
||||
self.v_cruise_helper.update_v_cruise(
|
||||
self.car_state(canonical_kph, cluster_kph, standstill=standstill, gas_pressed=gas_pressed, v_ego_kph=v_ego_kph, button_events=released),
|
||||
enabled=True,
|
||||
is_metric=is_metric,
|
||||
)
|
||||
|
||||
def set_increments(self, short_increment, long_increment):
|
||||
self.params.put("CustomAccShortPressIncrement", short_increment, block=True)
|
||||
self.params.put("CustomAccLongPressIncrement", long_increment, block=True)
|
||||
self.v_cruise_helper.read_custom_set_speed_params()
|
||||
|
||||
def assert_kph_almost_equal(self, actual, expected):
|
||||
self.assertAlmostEqual(actual, expected, delta=abs(expected) * 1e-6)
|
||||
|
||||
def route_button_events(self, payload):
|
||||
self.route_parser.update((1, [(0x361, bytes.fromhex(payload), 0)]))
|
||||
current = get_virtual_cruise_button(
|
||||
self.route_parser.vl["CLUTCH"]["CRUISE_RES"],
|
||||
self.route_parser.vl["CLUTCH"]["CRUISE_SET"],
|
||||
)
|
||||
events = create_button_events(current, self.route_button, VIRTUAL_CRUISE_BUTTONS)
|
||||
self.route_button = current
|
||||
return events
|
||||
|
||||
def test_short_press_rounds_display_target_and_preserves_offset(self):
|
||||
self.seed_enabled(27, 31)
|
||||
self.press(ButtonType.accelCruise, 28, 32)
|
||||
|
||||
assert self.v_cruise_helper.v_cruise_kph == 31
|
||||
assert self.v_cruise_helper.v_cruise_cluster_kph == 35
|
||||
|
||||
def test_decel_at_display_minimum_does_not_increase_target(self):
|
||||
self.seed_enabled(26, 30)
|
||||
self.press(ButtonType.decelCruise, 25, 29)
|
||||
|
||||
assert self.v_cruise_helper.v_cruise_kph == 26
|
||||
assert self.v_cruise_helper.v_cruise_cluster_kph == 30
|
||||
|
||||
@parameterized.expand((52, TOYOTA_VIRTUAL_CRUISE_LONG_PRESS - 1))
|
||||
def test_route_length_short_press_is_not_a_long_press(self, hold_frames):
|
||||
self.set_increments(short_increment=2, long_increment=5)
|
||||
self.seed_enabled(27, 31)
|
||||
self.press(ButtonType.accelCruise, 28, 32, hold_frames=hold_frames)
|
||||
|
||||
assert self.v_cruise_helper.v_cruise_kph == 29
|
||||
assert self.v_cruise_helper.v_cruise_cluster_kph == 33
|
||||
|
||||
def test_toyota_long_press_uses_route_validated_cadence_and_suppresses_release(self):
|
||||
self.set_increments(short_increment=2, long_increment=5)
|
||||
self.seed_enabled(27, 31)
|
||||
|
||||
pressed = [ButtonEvent(type=ButtonType.accelCruise, pressed=True)]
|
||||
self.v_cruise_helper.update_v_cruise(self.car_state(31, 35, button_events=pressed), enabled=True, is_metric=True)
|
||||
for _ in range(TOYOTA_VIRTUAL_CRUISE_LONG_PRESS):
|
||||
self.v_cruise_helper.update_v_cruise(self.car_state(31, 35), enabled=True, is_metric=True)
|
||||
|
||||
assert self.v_cruise_helper.v_cruise_kph == 31
|
||||
assert self.v_cruise_helper.v_cruise_cluster_kph == 35
|
||||
|
||||
released = [ButtonEvent(type=ButtonType.accelCruise, pressed=False)]
|
||||
self.v_cruise_helper.update_v_cruise(self.car_state(31, 35, button_events=released), enabled=True, is_metric=True)
|
||||
assert self.v_cruise_helper.v_cruise_kph == 31
|
||||
assert self.v_cruise_helper.v_cruise_cluster_kph == 35
|
||||
|
||||
def test_route_4_32_second_hold_repeats_six_times(self):
|
||||
self.seed_enabled(26, 30)
|
||||
self.press(ButtonType.accelCruise, 30, 34, hold_frames=432)
|
||||
|
||||
assert self.v_cruise_helper.v_cruise_kph == 56
|
||||
assert self.v_cruise_helper.v_cruise_cluster_kph == 60
|
||||
|
||||
def test_maximum_boundary_caps_pair_and_preserves_offset(self):
|
||||
self.seed_enabled(141, 145)
|
||||
self.press(ButtonType.accelCruise, 142, 146)
|
||||
|
||||
assert self.v_cruise_helper.v_cruise_kph == 141
|
||||
assert self.v_cruise_helper.v_cruise_cluster_kph == 145
|
||||
|
||||
self.press(ButtonType.accelCruise, 143, 147)
|
||||
assert self.v_cruise_helper.v_cruise_kph == 141
|
||||
assert self.v_cruise_helper.v_cruise_cluster_kph == 145
|
||||
|
||||
@parameterized.expand(
|
||||
(
|
||||
(25, 29, ButtonType.decelCruise),
|
||||
(141, 147, ButtonType.accelCruise),
|
||||
)
|
||||
)
|
||||
def test_out_of_range_raw_pair_is_not_moved_in_opposite_direction(self, canonical_kph, cluster_kph, button_type):
|
||||
self.seed_enabled(canonical_kph, cluster_kph)
|
||||
self.press(button_type, canonical_kph, cluster_kph)
|
||||
|
||||
assert self.v_cruise_helper.v_cruise_kph == canonical_kph
|
||||
assert self.v_cruise_helper.v_cruise_cluster_kph == cluster_kph
|
||||
|
||||
def test_imperial_increment_preserves_canonical_cluster_pair(self):
|
||||
self.seed_enabled(45, 50, is_metric=False)
|
||||
self.press(ButtonType.accelCruise, 46, 51, is_metric=False)
|
||||
|
||||
assert self.v_cruise_helper.v_cruise_kph == 51
|
||||
assert self.v_cruise_helper.v_cruise_cluster_kph == 56
|
||||
|
||||
def test_engagement_button_held_does_not_change_target(self):
|
||||
initial = self.car_state(27, 31)
|
||||
self.v_cruise_helper.update_v_cruise(initial, enabled=False, is_metric=True)
|
||||
|
||||
pressed = [ButtonEvent(type=ButtonType.decelCruise, pressed=True)]
|
||||
self.v_cruise_helper.update_v_cruise(self.car_state(27, 31, button_events=pressed), enabled=False, is_metric=True)
|
||||
for _ in range(TOYOTA_VIRTUAL_CRUISE_LONG_PRESS + 10):
|
||||
self.v_cruise_helper.update_v_cruise(self.car_state(28, 32), enabled=True, is_metric=True)
|
||||
|
||||
released = [ButtonEvent(type=ButtonType.decelCruise, pressed=False)]
|
||||
self.v_cruise_helper.update_v_cruise(self.car_state(28, 32, button_events=released), enabled=True, is_metric=True)
|
||||
self.v_cruise_helper.update_v_cruise(self.car_state(28, 32), enabled=True, is_metric=True)
|
||||
|
||||
assert self.v_cruise_helper.v_cruise_kph == 28
|
||||
assert self.v_cruise_helper.v_cruise_cluster_kph == 32
|
||||
|
||||
def test_delayed_pcm_target_seeds_before_software_ownership(self):
|
||||
invalid = self.car_state(0, 0)
|
||||
self.v_cruise_helper.update_v_cruise(invalid, enabled=False, is_metric=True)
|
||||
|
||||
release = [ButtonEvent(type=ButtonType.decelCruise, pressed=False)]
|
||||
for _ in range(4):
|
||||
self.v_cruise_helper.update_v_cruise(self.car_state(0, 0, button_events=release), enabled=True, is_metric=True)
|
||||
assert self.v_cruise_helper.v_cruise_kph == V_CRUISE_UNSET
|
||||
assert self.v_cruise_helper.v_cruise_cluster_kph == V_CRUISE_UNSET
|
||||
|
||||
self.v_cruise_helper.update_v_cruise(self.car_state(27, 31), enabled=True, is_metric=True)
|
||||
self.assert_kph_almost_equal(self.v_cruise_helper.v_cruise_kph, 27)
|
||||
self.assert_kph_almost_equal(self.v_cruise_helper.v_cruise_cluster_kph, 31)
|
||||
|
||||
self.v_cruise_helper.update_v_cruise(self.car_state(28, 32), enabled=True, is_metric=True)
|
||||
self.assert_kph_almost_equal(self.v_cruise_helper.v_cruise_kph, 27)
|
||||
self.assert_kph_almost_equal(self.v_cruise_helper.v_cruise_cluster_kph, 31)
|
||||
|
||||
def test_route_payload_short_press_drives_virtual_target(self):
|
||||
self.seed_enabled(27, 31)
|
||||
|
||||
pressed = self.route_button_events("a61a0000561a1a81")
|
||||
self.v_cruise_helper.update_v_cruise(self.car_state(27, 31, button_events=pressed), enabled=True, is_metric=True)
|
||||
for _ in range(52):
|
||||
self.v_cruise_helper.update_v_cruise(self.car_state(28, 32), enabled=True, is_metric=True)
|
||||
|
||||
released = self.route_button_events("861a0000561b1a81")
|
||||
self.v_cruise_helper.update_v_cruise(self.car_state(28, 32, button_events=released), enabled=True, is_metric=True)
|
||||
assert self.v_cruise_helper.v_cruise_kph == 31
|
||||
assert self.v_cruise_helper.v_cruise_cluster_kph == 35
|
||||
|
||||
def test_prius_route_payload_short_set_drives_virtual_target(self):
|
||||
self.seed_enabled(31, 35)
|
||||
|
||||
pressed = self.route_button_events("965f000056666585")
|
||||
self.v_cruise_helper.update_v_cruise(self.car_state(31, 35, button_events=pressed), enabled=True, is_metric=True)
|
||||
for _ in range(45):
|
||||
self.v_cruise_helper.update_v_cruise(self.car_state(30, 34), enabled=True, is_metric=True)
|
||||
|
||||
released = self.route_button_events("865f000056666585")
|
||||
self.v_cruise_helper.update_v_cruise(self.car_state(30, 34, button_events=released), enabled=True, is_metric=True)
|
||||
assert self.v_cruise_helper.v_cruise_kph == 26
|
||||
assert self.v_cruise_helper.v_cruise_cluster_kph == 30
|
||||
|
||||
def test_prius_route_payload_standstill_res_does_not_change_target(self):
|
||||
self.seed_enabled(27, 31)
|
||||
|
||||
pressed = self.route_button_events("a61b0000561c1c80")
|
||||
self.v_cruise_helper.update_v_cruise(
|
||||
self.car_state(27, 31, standstill=True, button_events=pressed),
|
||||
enabled=True,
|
||||
is_metric=True,
|
||||
)
|
||||
for _ in range(TOYOTA_VIRTUAL_CRUISE_LONG_PRESS):
|
||||
self.v_cruise_helper.update_v_cruise(self.car_state(27, 31, standstill=True), enabled=True, is_metric=True)
|
||||
|
||||
released = self.route_button_events("865f000056666585")
|
||||
self.v_cruise_helper.update_v_cruise(
|
||||
self.car_state(27, 31, standstill=True, button_events=released),
|
||||
enabled=True,
|
||||
is_metric=True,
|
||||
)
|
||||
assert self.v_cruise_helper.v_cruise_kph == 27
|
||||
assert self.v_cruise_helper.v_cruise_cluster_kph == 31
|
||||
|
||||
def test_route_payload_disengage_mid_hold_clears_pending_action(self):
|
||||
self.seed_enabled(27, 31)
|
||||
|
||||
pressed = self.route_button_events("a61a0000561a1a81")
|
||||
self.v_cruise_helper.update_v_cruise(self.car_state(27, 31, button_events=pressed), enabled=True, is_metric=True)
|
||||
for _ in range(30):
|
||||
self.v_cruise_helper.update_v_cruise(self.car_state(28, 32), enabled=True, is_metric=True)
|
||||
|
||||
self.v_cruise_helper.update_v_cruise(self.car_state(28, 32), enabled=False, is_metric=True)
|
||||
released = self.route_button_events("861a0000561b1a81")
|
||||
self.v_cruise_helper.update_v_cruise(self.car_state(28, 32, available=False, button_events=released), enabled=False, is_metric=True)
|
||||
|
||||
self.v_cruise_helper.update_v_cruise(self.car_state(28, 32), enabled=False, is_metric=True)
|
||||
self.v_cruise_helper.update_v_cruise(self.car_state(28, 32), enabled=True, is_metric=True)
|
||||
self.v_cruise_helper.update_v_cruise(self.car_state(28, 32), enabled=True, is_metric=True)
|
||||
self.assert_kph_almost_equal(self.v_cruise_helper.v_cruise_kph, 28)
|
||||
self.assert_kph_almost_equal(self.v_cruise_helper.v_cruise_cluster_kph, 32)
|
||||
|
||||
def test_standstill_resume_does_not_change_target(self):
|
||||
self.seed_enabled(27, 31)
|
||||
self.press(ButtonType.accelCruise, 27, 31, standstill=True)
|
||||
|
||||
assert self.v_cruise_helper.v_cruise_kph == 27
|
||||
assert self.v_cruise_helper.v_cruise_cluster_kph == 31
|
||||
|
||||
def test_disengagement_discards_virtual_target_and_reseeds_raw_pair(self):
|
||||
self.seed_enabled(27, 31)
|
||||
self.press(ButtonType.accelCruise, 28, 32)
|
||||
assert self.v_cruise_helper.v_cruise_kph == 31
|
||||
|
||||
raw = self.car_state(28, 32)
|
||||
self.v_cruise_helper.update_v_cruise(raw, enabled=False, is_metric=True)
|
||||
self.assert_kph_almost_equal(self.v_cruise_helper.v_cruise_kph, 28)
|
||||
self.assert_kph_almost_equal(self.v_cruise_helper.v_cruise_cluster_kph, 32)
|
||||
|
||||
self.v_cruise_helper.update_v_cruise(raw, enabled=True, is_metric=True)
|
||||
self.v_cruise_helper.update_v_cruise(raw, enabled=True, is_metric=True)
|
||||
self.assert_kph_almost_equal(self.v_cruise_helper.v_cruise_kph, 28)
|
||||
self.assert_kph_almost_equal(self.v_cruise_helper.v_cruise_cluster_kph, 32)
|
||||
|
||||
def test_unavailable_and_mads_handback_discard_virtual_target(self):
|
||||
self.seed_enabled(27, 31)
|
||||
self.press(ButtonType.accelCruise, 28, 32)
|
||||
assert self.v_cruise_helper.v_cruise_kph == 31
|
||||
|
||||
self.v_cruise_helper.update_v_cruise(self.car_state(28, 32), enabled=False, is_metric=True)
|
||||
self.assert_kph_almost_equal(self.v_cruise_helper.v_cruise_kph, 28)
|
||||
self.assert_kph_almost_equal(self.v_cruise_helper.v_cruise_cluster_kph, 32)
|
||||
|
||||
self.v_cruise_helper.update_v_cruise(self.car_state(0, 0, available=False), enabled=False, is_metric=True)
|
||||
assert self.v_cruise_helper.v_cruise_kph == V_CRUISE_UNSET
|
||||
assert self.v_cruise_helper.v_cruise_cluster_kph == V_CRUISE_UNSET
|
||||
|
||||
self.v_cruise_helper.update_v_cruise(self.car_state(29, 33), enabled=False, is_metric=True)
|
||||
self.assert_kph_almost_equal(self.v_cruise_helper.v_cruise_kph, 29)
|
||||
self.assert_kph_almost_equal(self.v_cruise_helper.v_cruise_cluster_kph, 33)
|
||||
|
||||
def test_set_during_gas_override_clips_target_to_ego_speed(self):
|
||||
self.seed_enabled(27, 31)
|
||||
self.press(ButtonType.decelCruise, 26, 30, gas_pressed=True, v_ego_kph=50)
|
||||
|
||||
assert self.v_cruise_helper.v_cruise_kph == 50
|
||||
assert self.v_cruise_helper.v_cruise_cluster_kph == 54
|
||||
|
||||
@@ -12,11 +12,13 @@ from openpilot.cereal import log, custom
|
||||
from opendbc.car import structs
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState
|
||||
from openpilot.sunnypilot import PARAMS_UPDATE_PERIOD
|
||||
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
||||
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.blinker_pause_lateral import BlinkerPauseLateral
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_v0 import LatControlTorque as LatControlTorqueV0
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.stopping_controller import StoppingController
|
||||
|
||||
|
||||
class ControlsExt(ModelStateBase):
|
||||
@@ -31,6 +33,11 @@ class ControlsExt(ModelStateBase):
|
||||
self.CP_SP = messaging.log_from_bytes(params.get("CarParamsSP", block=True), custom.CarParamsSP)
|
||||
cloudlog.info("controlsd_ext got CarParamsSP")
|
||||
|
||||
self.stopping_controller = StoppingController(self.CP.stopAccel) if params.get_bool("SunnypilotStoppingController") else None
|
||||
# the stock LongControl.update() has already run by the time long_accel_sp() sees it, so carry the pair it was
|
||||
# handed last frame; a fresh LongControl starts at (off, 0.0)
|
||||
self._stopping_prev = (LongCtrlState.off, 0.0)
|
||||
|
||||
self.sm_services_ext = ['radarState', 'selfdriveStateSP']
|
||||
self.pm_services_ext = ['carControlSP']
|
||||
|
||||
@@ -47,6 +54,27 @@ class ControlsExt(ModelStateBase):
|
||||
else:
|
||||
return lac
|
||||
|
||||
def long_accel_sp(self, actuators, CS, long_plan, accel_limits: tuple[float, float]) -> None:
|
||||
"""sunnypilot: terminal-stop policy applied after the stock LongControl.update() (see stopping_controller.py)."""
|
||||
stock_state = self.LoC.long_control_state
|
||||
stock_accel = actuators.accel
|
||||
prev_state, prev_accel = self._stopping_prev
|
||||
self._stopping_prev = (stock_state, stock_accel)
|
||||
|
||||
if self.stopping_controller is None:
|
||||
return
|
||||
|
||||
state, accel = self.stopping_controller.update(
|
||||
prev_state, stock_state, CS, long_plan.aTarget, prev_accel, stock_accel, accel_limits, long_plan.hasLead,
|
||||
pitch=self.calibrated_pose.orientation.pitch if self.calibrated_pose is not None else None,
|
||||
a_long=self.calibrated_pose.acceleration.x if self.calibrated_pose is not None else None)
|
||||
if state != stock_state:
|
||||
self.LoC.reset()
|
||||
self.LoC.long_control_state = state
|
||||
self.LoC.last_output_accel = accel
|
||||
self._stopping_prev = (state, accel)
|
||||
actuators.accel = float(accel)
|
||||
|
||||
def get_params_sp(self, sm: messaging.SubMaster) -> None:
|
||||
if time.monotonic() - self._param_update_time > PARAMS_UPDATE_PERIOD:
|
||||
self.blinker_pause_lateral.get_params()
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
from openpilot.cereal import custom
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.sunnypilot import get_sanitize_int_param
|
||||
|
||||
AccelProfile = custom.LongitudinalPlanSP.AccelController.Profile
|
||||
|
||||
MAX_ACCEL_BREAKPOINTS = [0., 3., 12, 24., 36.] # m/s
|
||||
MAX_ACCEL_PROFILES = {
|
||||
AccelProfile.eco: [1.75, 1.50, 0.40, 0.15, 0.12],
|
||||
AccelProfile.normal: [1.95, 1.80, 0.70, 0.36, 0.25],
|
||||
AccelProfile.sport: [2.00, 2.00, 1.20, 0.70, 0.50],
|
||||
}
|
||||
# Eco while the hybrid's engine is OFF (battery-first mode, ported from tncr18): keep wheel power m*a*v + road load under
|
||||
# the ~11-14 kW at which a Corolla hybrid starts its engine (30 Corolla Cross rlogs, threshold not SOC dependent):
|
||||
# 60 km/h 0.25, 70 km/h 0.16, 80 km/h 0.12. Launch values are the Altis eco ones. Once the engine runs there is nothing
|
||||
# to save, so the normal eco line takes over until it stops again.
|
||||
ECO_ENGINE_OFF_BP = [0., 3., 12., 16.67, 19.44, 22.22, 24., 36.] # m/s
|
||||
# The first two rows follow the eco line above: battery-first may never launch harder than eco. They still held the
|
||||
# pre-retune eco values (1.85 / 1.55) in the source this was ported from, which put engine-off above eco at 0-3 m/s.
|
||||
ECO_ENGINE_OFF_MAX_ACCEL = [1.75, 1.50, 0.35, 0.25, 0.16, 0.12, 0.08, 0.06]
|
||||
# Cruise deceleration to a lowered set speed (no lead). Normal/sport: one CONSTANT decel latched when the gap opens
|
||||
# (gap / response time, never gentler than CRUISE_DECEL_ACCEL) and held, so the car slows on a straight line instead of
|
||||
# the proportional gap/time law, which braked hardest at the start and dragged a long tail; more presses = firmer.
|
||||
# Eco (driver request 2026-09-25/26): never hurry to the new set speed however far it is or however many presses.
|
||||
# Above 70 km/h: pure coasting - the target decel IS the planner's pitch-aware coast accel (get_coast_accel), so the
|
||||
# PCM neither adds throttle nor regen; the speed table above 70 is only the fallback when no coast value is given
|
||||
# (road-load estimate of a 1500 kg Corolla Cross: -0.22 at 70, -0.30 at 90, -0.40 at 120). Below 60 km/h -0.40
|
||||
# (regen), blended into coasting between 60 and 70 as the speed falls. Whether -0.40 is too firm is for the road test.
|
||||
CRUISE_DECEL_RESPONSE_TIME = { # seconds to close the gap (normal/sport)
|
||||
AccelProfile.normal: 3.5,
|
||||
AccelProfile.sport: 3.0,
|
||||
}
|
||||
CRUISE_DECEL_ACCEL = { # m/s^2; gentlest constant decel (normal/sport)
|
||||
AccelProfile.normal: -0.50,
|
||||
AccelProfile.sport: -0.65,
|
||||
}
|
||||
ECO_CRUISE_DECEL_BP = [0., 16.67, 19.44, 25.0, 33.3] # m/s (0, 60, 70, 90, 120 km/h)
|
||||
ECO_CRUISE_DECEL_V = [-0.40, -0.40, -0.22, -0.30, -0.40] # m/s^2; the >= 70 km/h part is only the FALLBACK coast estimate
|
||||
ECO_COAST_BLEND_BP = [16.67, 19.44] # m/s: -0.40 at 60 km/h blends into pure coasting by 70 km/h
|
||||
ECO_COAST_MIN, ECO_COAST_MAX = -1.2, -0.05 # m/s^2; sanity clip on the planner's pitch-aware coast accel
|
||||
# Eco lead pull-away boost (driver 2026-10-01 night): below 80 km/h, when a detected lead is pulling away (gap opening),
|
||||
# eco may accelerate a little harder for a while so the car keeps up, eased in and out; with no lead nothing changes.
|
||||
# The boost scales with how fast the gap opens, fades out from 70 to 80 km/h, and never lifts eco above the normal line.
|
||||
ECO_BOOST_DV_BP = [0.3, 2.5] # m/s, lead speed minus ego speed
|
||||
ECO_BOOST_V = [0.0, 0.35] # m/s^2 added to the eco limit
|
||||
ECO_BOOST_FADE_BP = [19.44, 22.22] # m/s (70 -> 80 km/h): full -> none
|
||||
ECO_BOOST_D_MAX = 80.0 # m, only a lead this close counts
|
||||
ECO_BOOST_PROB_MIN = 0.5
|
||||
ECO_BOOST_RISE = 0.25 # m/s^3, how fast the boost builds
|
||||
ECO_BOOST_FALL = 0.5 # m/s^3, how fast it fades
|
||||
ECO_BOOST_DT = 0.05 # s, planner step
|
||||
CRUISE_DECEL_TAPER_TIME = 1.0 # s; inside |decel| * this of the target the decel eases off proportionally (no overshoot)
|
||||
|
||||
|
||||
class AccelController:
|
||||
def __init__(self):
|
||||
self.params = Params()
|
||||
self._cruise_decel: float | None = None
|
||||
self._cruise_decel_target: float | None = None
|
||||
self._boost = 0.0
|
||||
self.update()
|
||||
|
||||
def update(self) -> None:
|
||||
self._profile = get_sanitize_int_param("AccelPersonality", AccelProfile.eco, AccelProfile.sport, self.params)
|
||||
self._enabled = self.params.get_bool("AccelPersonalityEnabled")
|
||||
|
||||
@property
|
||||
def profile(self) -> int:
|
||||
return self._profile
|
||||
|
||||
def is_enabled(self) -> bool:
|
||||
return self._enabled
|
||||
|
||||
def get_max_accel(self, v_ego: float, engine_off: bool = False, lead=None) -> float:
|
||||
if engine_off and self._profile == AccelProfile.eco:
|
||||
base = float(np.interp(max(0.0, v_ego), ECO_ENGINE_OFF_BP, ECO_ENGINE_OFF_MAX_ACCEL))
|
||||
else:
|
||||
base = float(np.interp(max(0.0, v_ego), MAX_ACCEL_BREAKPOINTS, MAX_ACCEL_PROFILES[self._profile]))
|
||||
if self._profile != AccelProfile.eco:
|
||||
self._boost = 0.0
|
||||
return base
|
||||
return base + self._eco_lead_boost(v_ego, base, lead)
|
||||
|
||||
def _eco_lead_boost(self, v_ego: float, base: float, lead) -> float:
|
||||
target = 0.0
|
||||
if lead is not None and lead.present and lead.modelProb > ECO_BOOST_PROB_MIN and lead.dRel < ECO_BOOST_D_MAX:
|
||||
target = float(np.interp(lead.vLead - v_ego, ECO_BOOST_DV_BP, ECO_BOOST_V))
|
||||
target *= float(np.interp(v_ego, ECO_BOOST_FADE_BP, [1.0, 0.0]))
|
||||
normal = float(np.interp(max(0.0, v_ego), MAX_ACCEL_BREAKPOINTS, MAX_ACCEL_PROFILES[AccelProfile.normal]))
|
||||
target = float(np.clip(target, 0.0, max(normal - base, 0.0)))
|
||||
step = (ECO_BOOST_RISE if target > self._boost else ECO_BOOST_FALL) * ECO_BOOST_DT
|
||||
self._boost = float(np.clip(target, self._boost - step, self._boost + step))
|
||||
return self._boost
|
||||
|
||||
def get_cruise_target(self, v_ego: float, v_target: float, accel_coast: float | None = None) -> float:
|
||||
if not np.isfinite(v_target) or v_target <= 0.0 or v_target >= v_ego:
|
||||
self._cruise_decel = None
|
||||
self._cruise_decel_target = None
|
||||
return v_target
|
||||
|
||||
target_delta = v_target - v_ego
|
||||
if self._profile == AccelProfile.eco:
|
||||
# speed-scheduled, independent of the gap and of how many presses opened it
|
||||
decel = float(np.interp(v_ego, ECO_CRUISE_DECEL_BP, ECO_CRUISE_DECEL_V))
|
||||
if accel_coast is not None and np.isfinite(accel_coast) and accel_coast < 0.0:
|
||||
# >= 70 km/h: coast exactly (pitch-aware); 60-70 km/h: blend from the low-speed value into coasting
|
||||
coast = float(np.clip(accel_coast, ECO_COAST_MIN, ECO_COAST_MAX))
|
||||
w = float(np.interp(v_ego, ECO_COAST_BLEND_BP, [0.0, 1.0]))
|
||||
decel = (1.0 - w) * ECO_CRUISE_DECEL_V[1] + w * coast
|
||||
else:
|
||||
if self._cruise_decel is None or self._cruise_decel_target != v_target:
|
||||
# a new (or changed) gap: latch one constant decel for this episode
|
||||
self._cruise_decel = min(CRUISE_DECEL_ACCEL[self._profile], target_delta / CRUISE_DECEL_RESPONSE_TIME[self._profile])
|
||||
self._cruise_decel_target = v_target
|
||||
decel = self._cruise_decel
|
||||
|
||||
# taper only once the target is close (both terms are negative; max = the gentler one) so the speed lands cleanly
|
||||
decel = max(decel, target_delta / CRUISE_DECEL_TAPER_TIME)
|
||||
return float(v_ego + decel)
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import numpy as np
|
||||
|
||||
from opendbc.car.interfaces import ACCEL_MAX
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.accel_controller.accel_controller import (
|
||||
AccelController, AccelProfile, CRUISE_DECEL_ACCEL, CRUISE_DECEL_RESPONSE_TIME, ECO_CRUISE_DECEL_BP,
|
||||
ECO_CRUISE_DECEL_V, ECO_ENGINE_OFF_MAX_ACCEL, MAX_ACCEL_BREAKPOINTS, MAX_ACCEL_PROFILES,
|
||||
)
|
||||
|
||||
|
||||
class TestAccelController(OpenpilotTestCase):
|
||||
def setUp(self):
|
||||
self.params = Params()
|
||||
self.params.put_bool("AccelPersonalityEnabled", True, block=True)
|
||||
self.params.put("AccelPersonality", AccelProfile.normal, block=True)
|
||||
|
||||
def set_profile(self, profile: int) -> AccelController:
|
||||
self.params.put("AccelPersonality", profile, block=True)
|
||||
return AccelController()
|
||||
|
||||
def test_table_breakpoints(self):
|
||||
for profile, values in MAX_ACCEL_PROFILES.items():
|
||||
controller = self.set_profile(profile)
|
||||
for speed, expected in zip(MAX_ACCEL_BREAKPOINTS, values, strict=True):
|
||||
assert controller.get_max_accel(speed) == expected
|
||||
|
||||
def test_profiles_are_ordered_monotonic_and_bounded(self):
|
||||
controllers = {
|
||||
profile: self.set_profile(profile)
|
||||
for profile in (AccelProfile.eco, AccelProfile.normal, AccelProfile.sport)
|
||||
}
|
||||
previous = {profile: float("inf") for profile in controllers}
|
||||
|
||||
for speed in np.linspace(0.0, 55.0, 551):
|
||||
values = {profile: controller.get_max_accel(speed) for profile, controller in controllers.items()}
|
||||
assert 0.0 <= values[AccelProfile.eco] <= values[AccelProfile.normal] <= values[AccelProfile.sport] <= ACCEL_MAX
|
||||
for profile, value in values.items():
|
||||
assert value <= previous[profile]
|
||||
previous[profile] = value
|
||||
|
||||
def test_engine_off_line_only_lowers_eco(self):
|
||||
speeds = np.linspace(0.0, 40.0, 401)
|
||||
eco = self.set_profile(AccelProfile.eco)
|
||||
for speed in speeds:
|
||||
assert eco.get_max_accel(speed, engine_off=True) <= eco.get_max_accel(speed) + 1e-12
|
||||
for speed, expected in zip(MAX_ACCEL_BREAKPOINTS, ECO_ENGINE_OFF_MAX_ACCEL, strict=True):
|
||||
assert eco.get_max_accel(speed, engine_off=True) == expected
|
||||
|
||||
for profile in (AccelProfile.normal, AccelProfile.sport):
|
||||
controller = self.set_profile(profile)
|
||||
for speed in speeds:
|
||||
assert controller.get_max_accel(speed, engine_off=True) == controller.get_max_accel(speed)
|
||||
|
||||
def test_engine_off_eco_line_respects_hybrid_power_budget(self):
|
||||
controller = self.set_profile(AccelProfile.eco)
|
||||
for kph, budget_kw in ((60, 11.5), (70, 11.5), (80, 13.5)):
|
||||
speed = kph / 3.6
|
||||
road_load = (0.010 * 1500 * 9.81 + 0.5 * 1.2 * 0.82 * speed * speed) * speed
|
||||
wheel_power = 1500 * controller.get_max_accel(speed, engine_off=True) * speed + road_load
|
||||
assert wheel_power <= budget_kw * 1e3, kph
|
||||
|
||||
def test_lead_boost_is_eco_only_and_absent_without_a_lead(self):
|
||||
# the eco lead pull-away boost: no lead is the plain table lookup, and normal/sport ignore the lead entirely
|
||||
eco = self.set_profile(AccelProfile.eco)
|
||||
assert eco.get_max_accel(1.0, lead=None) == eco.get_max_accel(1.0)
|
||||
assert eco.get_max_accel(20.0, lead=None) == eco.get_max_accel(20.0)
|
||||
|
||||
normal = self.set_profile(AccelProfile.normal)
|
||||
lead = SimpleNamespace(present=True, modelProb=0.9, dRel=30.0, vLead=23.0)
|
||||
assert normal.get_max_accel(20.0, lead=lead) == normal.get_max_accel(20.0)
|
||||
|
||||
def test_sport_uses_openpilot_accel_max_at_launch(self):
|
||||
controller = self.set_profile(AccelProfile.sport)
|
||||
assert controller.get_max_accel(0.0) == ACCEL_MAX
|
||||
|
||||
def test_negative_speed_uses_standstill_value(self):
|
||||
controller = self.set_profile(AccelProfile.sport)
|
||||
assert controller.get_max_accel(-1.0) == MAX_ACCEL_PROFILES[AccelProfile.sport][0]
|
||||
|
||||
def test_eco_cruise_decel_is_speed_scheduled_not_gap_scheduled(self):
|
||||
controller = self.set_profile(AccelProfile.eco)
|
||||
a_small = controller.get_cruise_target(22.0, 20.0) - 22.0
|
||||
a_big = controller.get_cruise_target(22.0, 12.0) - 22.0
|
||||
assert np.isclose(a_small, a_big)
|
||||
assert np.isclose(controller.get_cruise_target(19.44, 10.0) - 19.44, -0.22)
|
||||
assert np.isclose(controller.get_cruise_target(15.0, 10.0) - 15.0, -0.40)
|
||||
assert -0.40 < controller.get_cruise_target(18.0, 10.0) - 18.0 < -0.22
|
||||
# with the planner's coast accel available, >= 70 km/h coasts exactly (pitch-aware), 60-70 blends into it
|
||||
assert np.isclose(controller.get_cruise_target(22.0, 10.0, accel_coast=-0.27) - 22.0, -0.27)
|
||||
assert np.isclose(controller.get_cruise_target(15.0, 10.0, accel_coast=-0.27) - 15.0, -0.40)
|
||||
assert -0.40 < controller.get_cruise_target(18.0, 10.0, accel_coast=-0.27) - 18.0 < -0.27
|
||||
# normal ignores the coast value
|
||||
normal = self.set_profile(AccelProfile.normal)
|
||||
assert normal.get_cruise_target(22.0, 20.0, accel_coast=-0.27) == normal.get_cruise_target(22.0, 20.0)
|
||||
def test_normal_and_sport_cruise_decel_latch_then_taper(self):
|
||||
for profile in (AccelProfile.normal, AccelProfile.sport):
|
||||
controller = self.set_profile(profile)
|
||||
v_target = 10.0
|
||||
a1 = controller.get_cruise_target(14.0, v_target) - 14.0
|
||||
a2 = controller.get_cruise_target(13.0, v_target) - 13.0
|
||||
expected = min(CRUISE_DECEL_ACCEL[profile], (v_target - 14.0) / CRUISE_DECEL_RESPONSE_TIME[profile])
|
||||
assert a1 < 0 and np.isclose(a1, a2)
|
||||
assert np.isclose(a1, expected)
|
||||
|
||||
a_close = controller.get_cruise_target(10.2, v_target) - 10.2
|
||||
assert a1 < a_close < 0
|
||||
assert controller.get_cruise_target(10.0, v_target) == v_target
|
||||
# A new, lower target re-latches a firmer deceleration.
|
||||
a_new_target = controller.get_cruise_target(14.0, v_target - 2.0) - 14.0
|
||||
assert a_new_target <= a1
|
||||
assert CRUISE_DECEL_RESPONSE_TIME[profile] >= 3.0
|
||||
|
||||
def test_non_decel_cruise_requests_clear_latched_deceleration(self):
|
||||
controller = self.set_profile(AccelProfile.normal)
|
||||
controller.get_cruise_target(14.0, 10.0)
|
||||
assert controller.get_cruise_target(10.0, 10.0) == 10.0
|
||||
assert np.isclose(controller.get_cruise_target(14.0, 10.0) - 14.0,
|
||||
(10.0 - 14.0) / CRUISE_DECEL_RESPONSE_TIME[AccelProfile.normal])
|
||||
|
||||
def test_eco_decel_table_is_valid(self):
|
||||
assert len(ECO_CRUISE_DECEL_BP) == len(ECO_CRUISE_DECEL_V)
|
||||
assert all(decel < 0.0 for decel in ECO_CRUISE_DECEL_V)
|
||||
|
||||
def test_params_refresh(self):
|
||||
controller = self.set_profile(AccelProfile.normal)
|
||||
self.params.put("AccelPersonality", AccelProfile.sport, block=True)
|
||||
controller.update()
|
||||
assert controller.profile == AccelProfile.sport
|
||||
|
||||
self.params.put_bool("AccelPersonalityEnabled", False, block=True)
|
||||
controller.update()
|
||||
assert not controller.is_enabled()
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
import numpy as np
|
||||
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.selfdrive.controls.lib.drive_helpers import should_stop
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_planner import A_CRUISE_MAX_BP, J_CRUISE_VALS, get_cruise_accel
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalPlanSource, T_IDXS as T_IDXS_MPC
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.accel_controller.accel_controller import (
|
||||
AccelController, AccelProfile, ECO_CRUISE_DECEL_BP, ECO_CRUISE_DECEL_V, MAX_ACCEL_BREAKPOINTS, MAX_ACCEL_PROFILES,
|
||||
)
|
||||
from openpilot.sunnypilot.selfdrive.test.longitudinal_maneuvers.plant import PRIUS_TSS2_ROUTE_MODEL, PlantSP
|
||||
|
||||
|
||||
class CarParams:
|
||||
steerRatio = 15.0
|
||||
wheelbase = 2.7
|
||||
|
||||
|
||||
def _set_mpc_acceleration(plant: PlantSP, acceleration: float = 2.0) -> None:
|
||||
def update(_radar_state, **_kwargs):
|
||||
mpc = plant.planner.mpc
|
||||
mpc.source = LongitudinalPlanSource.lead0
|
||||
mpc.v_solution[:] = mpc.x0[1] + acceleration * T_IDXS_MPC
|
||||
mpc.a_solution.fill(acceleration)
|
||||
mpc.j_solution.fill(0.0)
|
||||
|
||||
plant.planner.mpc.update = update
|
||||
|
||||
|
||||
def run_profile(profile: int, *, enabled: bool = True, speed: float = 0.0, v_cruise: float = 30.0,
|
||||
v_cruise_fn: Callable[[int], float] | None = None, e2e: bool = False, steps: int = 120,
|
||||
speed_noise: float = 0.0, seed: int = 0):
|
||||
params = Params()
|
||||
params.put_bool("AccelPersonalityEnabled", enabled, block=True)
|
||||
params.put("AccelPersonality", profile, block=True)
|
||||
controller = AccelController()
|
||||
rng = np.random.default_rng(seed)
|
||||
|
||||
accel = 0.0
|
||||
rows = []
|
||||
for frame in range(steps):
|
||||
target_speed = v_cruise if v_cruise_fn is None else v_cruise_fn(frame)
|
||||
measured = speed + (float(rng.normal(0.0, speed_noise)) if speed_noise else 0.0)
|
||||
accel = get_cruise_accel(e2e, target_speed, measured, accel, 0.0, CarParams(), DT_MDL, 2.0, True)
|
||||
if accel > 0.0:
|
||||
accel = min(accel, controller.get_max_accel(measured))
|
||||
speed = max(0.0, speed + accel * DT_MDL)
|
||||
rows.append((speed, accel, should_stop(speed, accel)))
|
||||
return rows
|
||||
|
||||
|
||||
def run_vehicle_profile(profile: int, duration: float = 80.0, enabled: bool = True, speed: float = 0.0,
|
||||
v_cruise_fn: Callable[[float], float] | None = None):
|
||||
params = Params()
|
||||
params.put_bool("AccelPersonalityEnabled", enabled, block=True)
|
||||
params.put("AccelPersonality", profile, block=True)
|
||||
|
||||
plant = PlantSP(speed=speed, actuator_model=PRIUS_TSS2_ROUTE_MODEL, run_long_control=True)
|
||||
_set_mpc_acceleration(plant)
|
||||
rows = []
|
||||
while plant.current_time < duration:
|
||||
v_cruise = 25.0 if v_cruise_fn is None else v_cruise_fn(plant.current_time)
|
||||
result = plant.step(v_cruise=v_cruise)
|
||||
rows.append((plant.current_time, result["speed"], result["a_target"], result["actuator_command"], result["acceleration"]))
|
||||
return np.asarray(rows)
|
||||
|
||||
|
||||
class TestAccelControllerClosedLoop(OpenpilotTestCase):
|
||||
def test_profiles_are_immediate_smooth_and_clearly_distinct(self):
|
||||
traces = {profile: run_vehicle_profile(profile) for profile in (AccelProfile.eco, AccelProfile.normal, AccelProfile.sport)}
|
||||
stock = run_vehicle_profile(AccelProfile.normal, enabled=False)
|
||||
|
||||
def crossing(trace, speed):
|
||||
return float(trace[np.flatnonzero(trace[:, 1] >= speed)[0], 0])
|
||||
|
||||
time_to_20 = {profile: crossing(trace, 20.0 * CV.MPH_TO_MS) for profile, trace in traces.items()}
|
||||
time_to_50 = {profile: crossing(trace, 50.0 * CV.MPH_TO_MS) for profile, trace in traces.items()}
|
||||
first_motion = {profile: int(np.flatnonzero(trace[:, 1] > 0.01)[0]) for profile, trace in traces.items()}
|
||||
|
||||
self.assertEqual(len(set(first_motion.values())), 1)
|
||||
self.assertTrue(all(trace[0, 2] > 0.0 and trace[1, 3] > 0.0 for trace in traces.values()))
|
||||
self.assertLess(time_to_20[AccelProfile.eco], 10.0)
|
||||
self.assertLess(time_to_50[AccelProfile.eco], 65.0)
|
||||
self.assertGreaterEqual(time_to_20[AccelProfile.eco] - time_to_20[AccelProfile.normal], 0.5)
|
||||
self.assertGreaterEqual(time_to_20[AccelProfile.normal] - time_to_20[AccelProfile.sport], 0.5)
|
||||
self.assertGreaterEqual(time_to_50[AccelProfile.eco] - time_to_50[AccelProfile.normal], 2.0)
|
||||
self.assertGreaterEqual(time_to_50[AccelProfile.normal] - time_to_50[AccelProfile.sport], 3.0)
|
||||
|
||||
stock_peak_jerk = float(np.max(np.abs(np.diff(stock[:, 3])) / DT_MDL))
|
||||
for profile, trace in traces.items():
|
||||
command_jerk = np.abs(np.diff(trace[:, 3])) / DT_MDL
|
||||
self.assertLessEqual(float(np.max(command_jerk)), stock_peak_jerk + 1e-9, profile)
|
||||
|
||||
settled = np.flatnonzero(trace[:, 1] >= 25.0 - 0.15)
|
||||
self.assertGreater(len(settled), 0)
|
||||
settled_trace = trace[settled[0]:]
|
||||
self.assertGreaterEqual(float(np.min(settled_trace[:, 3])), -0.05)
|
||||
self.assertLessEqual(float(np.max(trace[:, 1])), 25.0 + 1e-9)
|
||||
|
||||
def test_blended_positive_model_request_uses_profile_cruise_cap(self):
|
||||
params = Params()
|
||||
params.put_bool("DynamicExperimentalControl", False, block=True)
|
||||
params.put_bool("AccelPersonalityEnabled", True, block=True)
|
||||
params.put("AccelPersonality", AccelProfile.eco, block=True)
|
||||
|
||||
def request_acceleration(_current_time: float, _speed: float, _acceleration: float) -> tuple[float, bool]:
|
||||
return 2.0, False
|
||||
|
||||
plant = PlantSP(speed=15.0, e2e=True, model_action_fn=request_acceleration)
|
||||
_set_mpc_acceleration(plant)
|
||||
results = [plant.step(v_cruise=35.0) for _ in range(20)]
|
||||
settled = results[-1]
|
||||
eco_limit = 0.85 * float(np.interp(settled["published_v_ego"], MAX_ACCEL_BREAKPOINTS, MAX_ACCEL_PROFILES[AccelProfile.eco]))
|
||||
|
||||
self.assertTrue(settled["controller_active"])
|
||||
self.assertEqual(settled["mpc_source"], LongitudinalPlanSource.cruise)
|
||||
self.assertAlmostEqual(settled["a_target"], eco_limit, delta=0.01)
|
||||
self.assertLess(settled["a_target"], settled["model_action"]["desiredAcceleration"])
|
||||
|
||||
def test_normal_launch_is_faster_than_eco(self):
|
||||
eco = run_profile(AccelProfile.eco, speed=4.0, steps=120)
|
||||
normal = run_profile(AccelProfile.normal, speed=4.0, steps=120)
|
||||
self.assertGreater(normal[-1][0], eco[-1][0])
|
||||
|
||||
def test_zero_speed_stop_request_is_unchanged(self):
|
||||
for e2e in (False, True):
|
||||
stock = run_profile(AccelProfile.normal, enabled=False, speed=20.0, v_cruise=0.0, e2e=e2e, steps=100)
|
||||
for profile in (AccelProfile.eco, AccelProfile.normal, AccelProfile.sport):
|
||||
self.assertEqual(run_profile(profile, speed=20.0, v_cruise=0.0, e2e=e2e, steps=100), stock)
|
||||
|
||||
def test_eco_cruise_decel_uses_gentle_speed_schedule(self):
|
||||
params = Params()
|
||||
params.put_bool("AccelPersonalityEnabled", True, block=True)
|
||||
params.put("AccelPersonality", AccelProfile.eco, block=True)
|
||||
controller = AccelController()
|
||||
|
||||
speeds = (15.0, 18.0, 19.44, 25.0, 33.3)
|
||||
decels = [controller.get_cruise_target(speed, speed - 10.0) - speed for speed in speeds]
|
||||
expected = np.interp(speeds, ECO_CRUISE_DECEL_BP, ECO_CRUISE_DECEL_V)
|
||||
np.testing.assert_allclose(decels, expected)
|
||||
self.assertTrue(all(decel > -0.5 for decel in decels))
|
||||
|
||||
def test_normal_cruise_decel_remains_gradual(self):
|
||||
params = Params()
|
||||
params.put_bool("AccelPersonalityEnabled", True, block=True)
|
||||
params.put("AccelPersonality", AccelProfile.normal, block=True)
|
||||
controller = AccelController()
|
||||
target = 25.0 - 5.0 * CV.MPH_TO_MS
|
||||
stock = get_cruise_accel(False, target, 25.0, 0.0, 0.0, CarParams(), DT_MDL, 2.0, True)
|
||||
shaped = controller.get_cruise_target(25.0, target)
|
||||
self.assertLess(shaped, 25.0)
|
||||
self.assertGreater(shaped, target)
|
||||
self.assertLess(shaped - 25.0, 0.0)
|
||||
self.assertLess(shaped - 25.0, -0.3)
|
||||
self.assertGreater(abs(stock), 0.0)
|
||||
|
||||
def test_blended_launch_respects_profiles(self):
|
||||
traces = {
|
||||
profile: run_profile(profile, v_cruise=8.0, e2e=True, steps=180)
|
||||
for profile in (AccelProfile.eco, AccelProfile.normal, AccelProfile.sport)
|
||||
}
|
||||
time_to_five = {
|
||||
profile: next(frame for frame, row in enumerate(rows) if row[0] >= 5.0) * DT_MDL
|
||||
for profile, rows in traces.items()
|
||||
}
|
||||
|
||||
self.assertLess(time_to_five[AccelProfile.sport], time_to_five[AccelProfile.normal])
|
||||
self.assertLess(time_to_five[AccelProfile.normal], time_to_five[AccelProfile.eco])
|
||||
|
||||
def test_launch_ordering_without_departure_delay(self):
|
||||
stock = run_profile(AccelProfile.normal, enabled=False, v_cruise=8.0, steps=160)
|
||||
traces = {
|
||||
profile: run_profile(profile, v_cruise=8.0, steps=160)
|
||||
for profile in (AccelProfile.eco, AccelProfile.normal, AccelProfile.sport)
|
||||
}
|
||||
first_motion = {
|
||||
profile: next(frame for frame, row in enumerate(rows) if row[0] > 0.01)
|
||||
for profile, rows in traces.items()
|
||||
}
|
||||
time_to_five = {
|
||||
profile: next(frame for frame, row in enumerate(rows) if row[0] >= 5.0) * DT_MDL
|
||||
for profile, rows in traces.items()
|
||||
}
|
||||
stock_first_motion = next(frame for frame, row in enumerate(stock) if row[0] > 0.01)
|
||||
|
||||
self.assertEqual(len(set(first_motion.values())), 1)
|
||||
self.assertTrue(all(frame == stock_first_motion for frame in first_motion.values()))
|
||||
self.assertGreaterEqual(time_to_five[AccelProfile.eco] - time_to_five[AccelProfile.normal], 0.1)
|
||||
self.assertGreaterEqual(time_to_five[AccelProfile.normal], time_to_five[AccelProfile.sport])
|
||||
|
||||
def test_road_speed_catchup_stays_useful(self):
|
||||
traces = {
|
||||
profile: run_profile(profile, speed=20.0, v_cruise=30.0, steps=100)
|
||||
for profile in (AccelProfile.eco, AccelProfile.normal, AccelProfile.sport)
|
||||
}
|
||||
gains = {profile: rows[-1][0] - 20.0 for profile, rows in traces.items()}
|
||||
self.assertGreater(gains[AccelProfile.normal] - gains[AccelProfile.eco], 0.05)
|
||||
self.assertGreater(gains[AccelProfile.sport] - gains[AccelProfile.normal], 0.1)
|
||||
|
||||
def test_full_catchup_trace_respects_stock_jerk(self):
|
||||
for profile in (AccelProfile.eco, AccelProfile.normal, AccelProfile.sport):
|
||||
rows = run_profile(profile, v_cruise=30.0, steps=300)
|
||||
previous_speed = 0.0
|
||||
previous_accel = 0.0
|
||||
for speed, accel, _should_stop in rows:
|
||||
jerk_step = float(np.interp(previous_speed, A_CRUISE_MAX_BP, J_CRUISE_VALS)) * DT_MDL
|
||||
self.assertLessEqual(abs(accel - previous_accel), jerk_step + 1e-12)
|
||||
previous_speed = speed
|
||||
previous_accel = accel
|
||||
|
||||
def test_stop_release_frame_is_profile_independent(self):
|
||||
def target_speed(frame: int) -> float:
|
||||
return 0.0 if frame < 20 else 8.0
|
||||
|
||||
traces = {
|
||||
profile: run_profile(profile, v_cruise_fn=target_speed, steps=80)
|
||||
for profile in (AccelProfile.eco, AccelProfile.normal, AccelProfile.sport)
|
||||
}
|
||||
release_frames = {
|
||||
profile: next(frame for frame, row in enumerate(rows) if frame >= 20 and not row[2])
|
||||
for profile, rows in traces.items()
|
||||
}
|
||||
stock = run_profile(AccelProfile.normal, enabled=False, v_cruise_fn=target_speed, steps=80)
|
||||
stock_release_frame = next(frame for frame, row in enumerate(stock) if frame >= 20 and not row[2])
|
||||
self.assertEqual(len(set(release_frames.values())), 1)
|
||||
self.assertTrue(all(frame == stock_release_frame for frame in release_frames.values()))
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.accel_controller.accel_controller import (
|
||||
AccelController, AccelProfile, MAX_ACCEL_BREAKPOINTS, MAX_ACCEL_PROFILES,
|
||||
)
|
||||
|
||||
|
||||
class TestEcoLeadBoost(OpenpilotTestCase):
|
||||
"""Eco lead pull-away boost (driver 2026-10-01 night)."""
|
||||
|
||||
@staticmethod
|
||||
def _ac(profile):
|
||||
from types import SimpleNamespace
|
||||
ac = AccelController.__new__(AccelController)
|
||||
ac._profile, ac._enabled, ac._boost = profile, True, 0.0
|
||||
ac._cruise_decel = ac._cruise_decel_target = None
|
||||
return ac, SimpleNamespace
|
||||
|
||||
def test_no_lead_unchanged(self):
|
||||
ac, _ = self._ac(AccelProfile.eco)
|
||||
v = 15.0
|
||||
base = float(np.interp(v, MAX_ACCEL_BREAKPOINTS, MAX_ACCEL_PROFILES[AccelProfile.eco]))
|
||||
for _ in range(100):
|
||||
assert ac.get_max_accel(v, False, None) == base
|
||||
|
||||
def test_pulling_away_lead_boosts_smoothly_within_normal(self):
|
||||
ac, NS = self._ac(AccelProfile.eco)
|
||||
v = 15.0
|
||||
lead = NS(present=True, modelProb=0.9, dRel=30.0, vLead=v + 3.0)
|
||||
base = float(np.interp(v, MAX_ACCEL_BREAKPOINTS, MAX_ACCEL_PROFILES[AccelProfile.eco]))
|
||||
normal = float(np.interp(v, MAX_ACCEL_BREAKPOINTS, MAX_ACCEL_PROFILES[AccelProfile.normal]))
|
||||
out = [ac.get_max_accel(v, False, lead) for _ in range(60)]
|
||||
steps = np.diff([base] + out)
|
||||
assert max(steps) <= 0.25 * 0.05 + 1e-9 # eased in
|
||||
assert out[-1] > base + 0.2 and out[-1] <= normal + 1e-9
|
||||
lead.present = False
|
||||
out2 = [ac.get_max_accel(v, False, lead) for _ in range(60)]
|
||||
assert min(np.diff([out[-1]] + out2)) >= -0.5 * 0.05 - 1e-9 # eased out
|
||||
assert abs(out2[-1] - base) < 1e-9
|
||||
|
||||
def test_no_boost_at_80_kph_or_for_a_closing_lead(self):
|
||||
ac, NS = self._ac(AccelProfile.eco)
|
||||
v = 80 / 3.6
|
||||
lead = NS(present=True, modelProb=0.9, dRel=30.0, vLead=v + 3.0)
|
||||
base = float(np.interp(v, MAX_ACCEL_BREAKPOINTS, MAX_ACCEL_PROFILES[AccelProfile.eco]))
|
||||
assert abs([ac.get_max_accel(v, False, lead) for _ in range(40)][-1] - base) < 1e-9
|
||||
ac, NS = self._ac(AccelProfile.eco)
|
||||
v = 15.0
|
||||
lead = NS(present=True, modelProb=0.9, dRel=30.0, vLead=v - 1.0)
|
||||
base = float(np.interp(v, MAX_ACCEL_BREAKPOINTS, MAX_ACCEL_PROFILES[AccelProfile.eco]))
|
||||
assert abs([ac.get_max_accel(v, False, lead) for _ in range(40)][-1] - base) < 1e-9
|
||||
|
||||
|
||||
class _Lead:
|
||||
def __init__(self, present=True, modelProb=0.9, dRel=30.0, vLead=18.0):
|
||||
self.present, self.modelProb, self.dRel, self.vLead = present, modelProb, dRel, vLead
|
||||
|
||||
|
||||
class _Radar:
|
||||
def __init__(self, lead):
|
||||
self.leadOne = lead
|
||||
|
||||
|
||||
class _Sink:
|
||||
def clear(self):
|
||||
pass
|
||||
|
||||
def update(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class TestPlannerLeadWiring(OpenpilotTestCase):
|
||||
"""The boost needs a lead, so the planner has to carry radarState.leadOne into get_max_accel_override."""
|
||||
|
||||
def setUp(self):
|
||||
self.params = Params()
|
||||
self.params.put_bool("AccelPersonalityEnabled", True, block=True)
|
||||
self.params.put("AccelPersonality", AccelProfile.eco, block=True)
|
||||
|
||||
@staticmethod
|
||||
def _planner():
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlannerSP
|
||||
|
||||
planner = LongitudinalPlannerSP.__new__(LongitudinalPlannerSP)
|
||||
planner.accel_controller = AccelController()
|
||||
planner.accel_controller_active = False
|
||||
planner.lead_one = None
|
||||
return planner
|
||||
|
||||
@staticmethod
|
||||
def _settle(planner, v_ego, lead=None, n=60):
|
||||
return [planner.get_max_accel_override(v_ego, lead=lead) for _ in range(n)][-1]
|
||||
|
||||
def test_update_stores_the_radar_lead(self):
|
||||
planner = self._planner()
|
||||
planner.events_sp = _Sink()
|
||||
planner.e2e_alerts_helper = _Sink()
|
||||
lead = _Lead()
|
||||
planner.update({"radarState": _Radar(lead)})
|
||||
assert planner.lead_one is lead
|
||||
|
||||
def test_stored_lead_feeds_the_boost_and_an_explicit_lead_wins(self):
|
||||
v = 15.0
|
||||
lead = _Lead(vLead=v + 3.0)
|
||||
planner = self._planner()
|
||||
planner.lead_one = lead
|
||||
boosted = self._settle(planner, v)
|
||||
|
||||
planner.accel_controller._boost = 0.0
|
||||
planner.lead_one = _Lead(present=False)
|
||||
base = self._settle(planner, v)
|
||||
assert boosted > base + 0.2
|
||||
|
||||
planner.accel_controller._boost = 0.0
|
||||
explicit = self._settle(planner, v, lead=lead)
|
||||
assert abs(explicit - boosted) < 1e-9
|
||||
|
||||
def test_disabled_accel_controller_returns_no_override(self):
|
||||
self.params.put_bool("AccelPersonalityEnabled", False, block=True)
|
||||
assert self._planner().get_max_accel_override(15.0, lead=_Lead()) is None
|
||||
@@ -1,17 +0,0 @@
|
||||
class WMACConstants:
|
||||
# Lead detection parameters
|
||||
LEAD_WINDOW_SIZE = 6 # Stable detection window
|
||||
LEAD_PROB = 0.45 # Balanced threshold for lead detection
|
||||
|
||||
# Slow down detection parameters
|
||||
SLOW_DOWN_WINDOW_SIZE = 5 # Responsive but stable
|
||||
SLOW_DOWN_PROB = 0.3 # Balanced threshold for slow down scenarios
|
||||
|
||||
# Optimized slow down distance curve - smooth and progressive
|
||||
SLOW_DOWN_BP = [0., 10., 20., 30., 40., 50., 55., 60.]
|
||||
SLOW_DOWN_DIST = [32., 46., 64., 86., 108., 130., 145., 165.]
|
||||
|
||||
# Slowness detection parameters
|
||||
SLOWNESS_WINDOW_SIZE = 10 # Stable slowness detection
|
||||
SLOWNESS_PROB = 0.55 # Clear threshold for slowness
|
||||
SLOWNESS_CRUISE_OFFSET = 1.025 # Conservative cruise speed offset
|
||||
@@ -4,192 +4,120 @@ Copyright (c) 2021-, rav4kumar, sunnypilot, and a number of other contributors.
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
# Version = 2025-6-30
|
||||
from dataclasses import dataclass
|
||||
from typing import Literal
|
||||
|
||||
import numpy as np
|
||||
|
||||
from openpilot.cereal import messaging
|
||||
from opendbc.car import structs
|
||||
from numpy import interp
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.dec.constants import WMACConstants
|
||||
from typing import Literal
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
|
||||
# d-e2e, from modeldata.h
|
||||
TRAJECTORY_SIZE = 33
|
||||
SET_MODE_TIMEOUT = 15
|
||||
|
||||
# Define the valid mode types
|
||||
ModeType = Literal['acc', 'blended']
|
||||
|
||||
_DECEL_LOOKAHEAD_MIN_T = 1.0
|
||||
_DECEL_LOOKAHEAD_MAX_T = 6.0
|
||||
_T_IDXS = np.array(ModelConstants.T_IDXS)
|
||||
_DECEL_IDX = np.where((_T_IDXS >= _DECEL_LOOKAHEAD_MIN_T) & (_T_IDXS <= _DECEL_LOOKAHEAD_MAX_T))[0]
|
||||
_DECEL_INV_T = 1.0 / _T_IDXS[_DECEL_IDX]
|
||||
|
||||
class SmoothKalmanFilter:
|
||||
"""Enhanced Kalman filter with smoothing for stable decision making."""
|
||||
DECEL_INTENT_A_HINT = 0.35
|
||||
DECEL_INTENT_A_FULL = 1.30
|
||||
DECEL_INTENT_TRIGGER = 0.5
|
||||
DECEL_INTENT_CURVE_OVERRIDE = 0.9
|
||||
|
||||
def __init__(self, initial_value=0, measurement_noise=0.1, process_noise=0.01,
|
||||
alpha=1.0, smoothing_factor=0.85):
|
||||
self.x = initial_value
|
||||
self.P = 1.0
|
||||
self.R = measurement_noise
|
||||
self.Q = process_noise
|
||||
self.alpha = alpha
|
||||
self.smoothing_factor = smoothing_factor
|
||||
self.initialized = False
|
||||
self.history = []
|
||||
self.max_history = 10
|
||||
self.confidence = 0.0
|
||||
CURVE_Y_MAX = 5.0
|
||||
|
||||
def add_data(self, measurement):
|
||||
if len(self.history) >= self.max_history:
|
||||
self.history.pop(0)
|
||||
self.history.append(measurement)
|
||||
LEAD_FUTURE_PROB_VANISH = 0.35
|
||||
LEAD_VETO_CONFIRM_FRAMES = 4
|
||||
|
||||
if not self.initialized:
|
||||
self.x = measurement
|
||||
self.initialized = True
|
||||
self.confidence = 0.1
|
||||
return
|
||||
MODEL_DROP_TRUST_FULL = 5.0
|
||||
MODEL_DROP_TRUST_NONE = 30.0
|
||||
MODEL_TRUST_MIN = 0.5
|
||||
|
||||
self.P = self.alpha * self.P + self.Q
|
||||
CREEP_SPEED_ENTER = 2.0
|
||||
CREEP_SPEED_EXIT = 3.0
|
||||
|
||||
K = self.P / (self.P + self.R)
|
||||
effective_K = K * (1.0 - self.smoothing_factor) + self.smoothing_factor * 0.1
|
||||
ENTER_FRAMES = 3
|
||||
EXIT_FRAMES = 16
|
||||
MIN_BLENDED_FRAMES = 20
|
||||
|
||||
innovation = measurement - self.x
|
||||
self.x = self.x + effective_K * innovation
|
||||
self.P = (1 - effective_K) * self.P
|
||||
|
||||
if abs(innovation) < 0.1:
|
||||
self.confidence = min(1.0, self.confidence + 0.05)
|
||||
else:
|
||||
self.confidence = max(0.1, self.confidence - 0.02)
|
||||
|
||||
def get_value(self):
|
||||
return self.x if self.initialized else None
|
||||
|
||||
def get_confidence(self):
|
||||
return self.confidence
|
||||
|
||||
def reset_data(self):
|
||||
self.initialized = False
|
||||
self.history = []
|
||||
self.confidence = 0.0
|
||||
PARAM_READ_FRAMES = 5
|
||||
|
||||
|
||||
class ModeTransitionManager:
|
||||
"""Manages smooth transitions between driving modes with hysteresis."""
|
||||
@dataclass
|
||||
class DecSignals:
|
||||
decel_intent: float = 0.0
|
||||
curve_detected: bool = False
|
||||
model_trust: float = 1.0
|
||||
creeping: bool = False
|
||||
|
||||
|
||||
def should_blend(s: DecSignals) -> bool:
|
||||
degraded = s.model_trust < MODEL_TRUST_MIN
|
||||
curve_gate = s.decel_intent >= DECEL_INTENT_CURVE_OVERRIDE or not s.curve_detected
|
||||
slowdown_detected = not degraded and s.decel_intent >= DECEL_INTENT_TRIGGER and curve_gate
|
||||
return slowdown_detected or s.creeping
|
||||
|
||||
|
||||
class ModeHysteresis:
|
||||
def __init__(self):
|
||||
self.current_mode: ModeType = 'acc'
|
||||
self.mode_confidence = {'acc': 1.0, 'blended': 0.0}
|
||||
self.transition_timeout = 0
|
||||
self.min_mode_duration = 10
|
||||
self.mode_duration = 0
|
||||
self.emergency_override = False
|
||||
self.mode: ModeType = 'acc'
|
||||
self.above = 0
|
||||
self.below = 0
|
||||
self.blended_frames = 0
|
||||
|
||||
def request_mode(self, mode: ModeType, confidence: float = 1.0, emergency: bool = False):
|
||||
# Emergency override for critical situations (stops, collisions)
|
||||
if emergency:
|
||||
self.emergency_override = True
|
||||
self.current_mode = mode
|
||||
self.transition_timeout = SET_MODE_TIMEOUT
|
||||
self.mode_duration = 0
|
||||
return
|
||||
def update(self, want_blended: bool, override: bool, veto: bool) -> ModeType:
|
||||
self.above = self.above + 1 if want_blended else 0
|
||||
self.below = 0 if want_blended else self.below + 1
|
||||
|
||||
self.mode_confidence[mode] = min(1.0, self.mode_confidence[mode] + 0.1 * confidence)
|
||||
for m in self.mode_confidence:
|
||||
if m != mode:
|
||||
self.mode_confidence[m] = max(0.0, self.mode_confidence[m] - 0.05)
|
||||
if override:
|
||||
self.mode, self.blended_frames = 'blended', 0
|
||||
elif veto:
|
||||
self.mode = 'acc'
|
||||
elif self.mode == 'acc':
|
||||
if self.above >= ENTER_FRAMES:
|
||||
self.mode, self.blended_frames = 'blended', 0
|
||||
else:
|
||||
self.blended_frames += 1
|
||||
if self.blended_frames >= MIN_BLENDED_FRAMES and self.below >= EXIT_FRAMES:
|
||||
self.mode = 'acc'
|
||||
return self.mode
|
||||
|
||||
# Require minimum duration in current mode (unless emergency)
|
||||
if self.mode_duration < self.min_mode_duration and not self.emergency_override:
|
||||
return
|
||||
|
||||
# Hysteresis: higher threshold for mode changes
|
||||
confidence_threshold = 0.6 if mode != self.current_mode else 0.3 # Lower threshold for faster response
|
||||
|
||||
if self.mode_confidence[mode] > confidence_threshold:
|
||||
if mode != self.current_mode and self.transition_timeout == 0:
|
||||
self.transition_timeout = SET_MODE_TIMEOUT
|
||||
self.current_mode = mode
|
||||
self.mode_duration = 0
|
||||
|
||||
def update(self):
|
||||
if self.transition_timeout > 0:
|
||||
self.transition_timeout -= 1
|
||||
self.mode_duration += 1
|
||||
|
||||
# Reset emergency override after some time
|
||||
if self.emergency_override and self.mode_duration > 20:
|
||||
self.emergency_override = False
|
||||
|
||||
# Gradual confidence decay
|
||||
for mode in self.mode_confidence:
|
||||
self.mode_confidence[mode] *= 0.98
|
||||
|
||||
def get_mode(self) -> ModeType:
|
||||
return self.current_mode
|
||||
def reset(self) -> None:
|
||||
self.mode = 'acc'
|
||||
self.above = 0
|
||||
self.below = 0
|
||||
self.blended_frames = 0
|
||||
|
||||
|
||||
class DynamicExperimentalController:
|
||||
def __init__(self, CP: structs.CarParams, mpc, params=None):
|
||||
self._CP = CP
|
||||
self._mpc = mpc
|
||||
self._params = params or Params()
|
||||
self._enabled: bool = self._params.get_bool("DynamicExperimentalControl")
|
||||
self._active: bool = False
|
||||
self._frame: int = 0
|
||||
self._urgency = 0.0
|
||||
|
||||
self._mode_manager = ModeTransitionManager()
|
||||
self._hysteresis = ModeHysteresis()
|
||||
self._creeping = False
|
||||
self._lead_veto_frames = 0
|
||||
|
||||
# Smooth filters for stable decision making with faster response for critical scenarios
|
||||
self._lead_filter = SmoothKalmanFilter(
|
||||
measurement_noise=0.15,
|
||||
process_noise=0.05,
|
||||
alpha=1.02,
|
||||
smoothing_factor=0.8
|
||||
)
|
||||
self.signals = DecSignals()
|
||||
self.want_blended = False
|
||||
self.lead_veto = False
|
||||
|
||||
self._slow_down_filter = SmoothKalmanFilter(
|
||||
measurement_noise=0.1,
|
||||
process_noise=0.1,
|
||||
alpha=1.05,
|
||||
smoothing_factor=0.7
|
||||
)
|
||||
|
||||
self._slowness_filter = SmoothKalmanFilter(
|
||||
measurement_noise=0.1,
|
||||
process_noise=0.06,
|
||||
alpha=1.015,
|
||||
smoothing_factor=0.92
|
||||
)
|
||||
|
||||
self._mpc_fcw_filter = SmoothKalmanFilter(
|
||||
measurement_noise=0.2,
|
||||
process_noise=0.1,
|
||||
alpha=1.1,
|
||||
smoothing_factor=0.5
|
||||
)
|
||||
self._has_lead_filtered = False
|
||||
self._has_slow_down = False
|
||||
self._has_slowness = False
|
||||
self._has_mpc_fcw = False
|
||||
self._v_ego_kph = 0.0
|
||||
self._v_cruise_kph = 0.0
|
||||
self._has_standstill = False
|
||||
self._mpc_fcw_crash_cnt = 0
|
||||
self._standstill_count = 0
|
||||
# debug
|
||||
self._endpoint_x = float('inf')
|
||||
self._expected_distance = 0.0
|
||||
self._trajectory_valid = False
|
||||
def _update_creeping(self, v_ego: float) -> bool:
|
||||
self._creeping = v_ego < CREEP_SPEED_EXIT if self._creeping else v_ego <= CREEP_SPEED_ENTER
|
||||
return self._creeping
|
||||
|
||||
def _read_params(self) -> None:
|
||||
if self._frame % int(1. / DT_MDL) == 0:
|
||||
if self._frame % PARAM_READ_FRAMES == 0:
|
||||
self._enabled = self._params.get_bool("DynamicExperimentalControl")
|
||||
|
||||
def mode(self) -> str:
|
||||
return self._mode_manager.get_mode()
|
||||
return self._hysteresis.mode
|
||||
|
||||
def enabled(self) -> bool:
|
||||
return self._enabled
|
||||
@@ -197,192 +125,77 @@ class DynamicExperimentalController:
|
||||
def active(self) -> bool:
|
||||
return self._active
|
||||
|
||||
def set_mpc_fcw_crash_cnt(self) -> None:
|
||||
"""Set MPC FCW crash count"""
|
||||
self._mpc_fcw_crash_cnt = self._mpc.crash_cnt
|
||||
@staticmethod
|
||||
def _decel_intent(md) -> float:
|
||||
v = np.asarray(md.velocity.x)
|
||||
if len(v) != len(_T_IDXS):
|
||||
return 0.0
|
||||
a_req = float(np.min((v[_DECEL_IDX] - v[0]) * _DECEL_INV_T))
|
||||
return float(np.interp(-a_req, [DECEL_INTENT_A_HINT, DECEL_INTENT_A_FULL], [0.0, 1.0]))
|
||||
|
||||
def _update_calculations(self, sm: messaging.SubMaster) -> None:
|
||||
car_state = sm['carState']
|
||||
lead_one = sm['radarState'].leadOne
|
||||
md = sm['modelV2']
|
||||
@staticmethod
|
||||
def _curve_detected(md) -> bool:
|
||||
y = md.position.y
|
||||
if len(y) < 1:
|
||||
return False
|
||||
return abs(y[-1]) >= CURVE_Y_MAX
|
||||
|
||||
self._v_ego_kph = car_state.vEgo * 3.6
|
||||
self._v_cruise_kph = car_state.vCruise
|
||||
self._has_standstill = car_state.standstill
|
||||
@staticmethod
|
||||
def _model_trust(md) -> float:
|
||||
if len(md.velocity.x) != len(_T_IDXS):
|
||||
return 0.0
|
||||
return float(np.interp(md.frameDropPerc, [MODEL_DROP_TRUST_FULL, MODEL_DROP_TRUST_NONE], [1.0, 0.0]))
|
||||
|
||||
# standstill detection
|
||||
if self._has_standstill:
|
||||
self._standstill_count = min(20, self._standstill_count + 1)
|
||||
else:
|
||||
self._standstill_count = max(0, self._standstill_count - 1)
|
||||
@staticmethod
|
||||
def _lead_veto(radar_state, md) -> bool:
|
||||
lead_one, lead_two = radar_state.leadOne, radar_state.leadTwo
|
||||
lead_now = lead_one.present or lead_two.present
|
||||
probs = md.leadsV3
|
||||
future = min(probs[1].prob, probs[2].prob) if len(probs) >= 3 else 1.0
|
||||
return bool(lead_now and future > LEAD_FUTURE_PROB_VANISH)
|
||||
|
||||
# Lead detection
|
||||
self._lead_filter.add_data(float(lead_one.present))
|
||||
lead_value = self._lead_filter.get_value() or 0.0
|
||||
self._has_lead_filtered = lead_value > WMACConstants.LEAD_PROB
|
||||
def _update_lead_veto(self, raw_veto: bool, lead_present: bool, urgent_override: bool) -> bool:
|
||||
if raw_veto:
|
||||
self._lead_veto_frames = min(self._lead_veto_frames + 1, LEAD_VETO_CONFIRM_FRAMES)
|
||||
return self.lead_veto or self._lead_veto_frames >= LEAD_VETO_CONFIRM_FRAMES
|
||||
|
||||
# MPC FCW detection
|
||||
fcw_filtered_value = self._mpc_fcw_filter.get_value() or 0.0
|
||||
self._mpc_fcw_filter.add_data(float(self._mpc_fcw_crash_cnt > 0))
|
||||
self._has_mpc_fcw = fcw_filtered_value > 0.5
|
||||
if not lead_present or urgent_override or not self.lead_veto:
|
||||
self._lead_veto_frames = 0
|
||||
return False
|
||||
|
||||
# Slow down detection
|
||||
self._calculate_slow_down(md)
|
||||
|
||||
# Slowness detection
|
||||
if not (self._standstill_count > 5) and not self._has_slow_down:
|
||||
current_slowness = float(self._v_ego_kph <= (self._v_cruise_kph * WMACConstants.SLOWNESS_CRUISE_OFFSET))
|
||||
self._slowness_filter.add_data(current_slowness)
|
||||
slowness_value = self._slowness_filter.get_value() or 0.0
|
||||
|
||||
# Hysteresis for slowness
|
||||
threshold = WMACConstants.SLOWNESS_PROB * (0.8 if self._has_slowness else 1.1)
|
||||
self._has_slowness = slowness_value > threshold
|
||||
|
||||
def _calculate_slow_down(self, md):
|
||||
"""Calculate urgency based on trajectory endpoint vs expected distance."""
|
||||
|
||||
# Reset to safe defaults
|
||||
urgency = 0.0
|
||||
self._endpoint_x = float('inf')
|
||||
self._trajectory_valid = False
|
||||
|
||||
#Require exact trajectory size
|
||||
position_valid = len(md.position.x) == TRAJECTORY_SIZE
|
||||
orientation_valid = len(md.orientation.x) == TRAJECTORY_SIZE
|
||||
|
||||
if not (position_valid and orientation_valid):
|
||||
# Invalid trajectory - this itself might indicate a stop scenario
|
||||
# Apply moderate urgency for incomplete trajectories at speed
|
||||
if self._v_ego_kph > 20.0:
|
||||
urgency = 0.3
|
||||
|
||||
self._slow_down_filter.add_data(urgency)
|
||||
urgency_filtered = self._slow_down_filter.get_value() or 0.0
|
||||
self._has_slow_down = urgency_filtered > WMACConstants.SLOW_DOWN_PROB
|
||||
self._urgency = urgency_filtered
|
||||
return
|
||||
|
||||
# We have a valid full trajectory
|
||||
self._trajectory_valid = True
|
||||
|
||||
# Use the exact endpoint (33rd point, index 32)
|
||||
endpoint_x = md.position.x[TRAJECTORY_SIZE - 1]
|
||||
self._endpoint_x = endpoint_x
|
||||
|
||||
# Get expected distance based on current speed using tuned constants
|
||||
expected_distance = interp(self._v_ego_kph,
|
||||
WMACConstants.SLOW_DOWN_BP,
|
||||
WMACConstants.SLOW_DOWN_DIST)
|
||||
self._expected_distance = expected_distance
|
||||
|
||||
# Calculate urgency based on trajectory shortage
|
||||
if endpoint_x < expected_distance:
|
||||
shortage = expected_distance - endpoint_x
|
||||
shortage_ratio = shortage / expected_distance
|
||||
|
||||
# Base urgency on shortage ratio
|
||||
urgency = min(1.0, shortage_ratio * 2.0)
|
||||
|
||||
# Increase urgency for very short trajectories (imminent stops)
|
||||
critical_distance = expected_distance * 0.3
|
||||
if endpoint_x < critical_distance:
|
||||
urgency = min(1.0, urgency * 2.0)
|
||||
|
||||
# Speed-based urgency adjustment
|
||||
if self._v_ego_kph > 25.0:
|
||||
speed_factor = 1.0 + (self._v_ego_kph - 25.0) / 80.0
|
||||
urgency = min(1.0, urgency * speed_factor)
|
||||
|
||||
# Apply filtering but with less smoothing for stops
|
||||
self._slow_down_filter.add_data(urgency)
|
||||
urgency_filtered = self._slow_down_filter.get_value() or 0.0
|
||||
|
||||
# Update state with lower threshold for better stop detection
|
||||
self._has_slow_down = urgency_filtered > (WMACConstants.SLOW_DOWN_PROB * 0.8)
|
||||
self._urgency = urgency_filtered
|
||||
|
||||
def _radarless_mode(self) -> None:
|
||||
"""Radarless mode decision logic with emergency handling."""
|
||||
|
||||
# EMERGENCY: MPC FCW - immediate blended mode
|
||||
if self._has_mpc_fcw:
|
||||
self._mode_manager.request_mode('blended', confidence=1.0, emergency=True)
|
||||
return
|
||||
|
||||
# Standstill: use blended
|
||||
if self._standstill_count > 3:
|
||||
self._mode_manager.request_mode('blended', confidence=0.9)
|
||||
return
|
||||
|
||||
# Slow down scenarios: emergency for high urgency, normal for lower urgency
|
||||
if self._has_slow_down:
|
||||
if self._urgency > 0.7:
|
||||
# Emergency: immediate blended mode for high urgency stops
|
||||
self._mode_manager.request_mode('blended', confidence=1.0, emergency=True)
|
||||
else:
|
||||
# Normal: blended with urgency-based confidence
|
||||
confidence = min(1.0, self._urgency * 1.5)
|
||||
self._mode_manager.request_mode('blended', confidence=confidence)
|
||||
return
|
||||
|
||||
# Driving slow: use ACC (but not if actively slowing down)
|
||||
if self._has_slowness and not self._has_slow_down:
|
||||
self._mode_manager.request_mode('acc', confidence=0.8)
|
||||
return
|
||||
|
||||
# Default: ACC
|
||||
self._mode_manager.request_mode('acc', confidence=0.7)
|
||||
|
||||
def _radar_mode(self) -> None:
|
||||
"""Radar mode with emergency handling."""
|
||||
|
||||
# EMERGENCY: MPC FCW - immediate blended mode
|
||||
if self._has_mpc_fcw:
|
||||
self._mode_manager.request_mode('blended', confidence=1.0, emergency=True)
|
||||
return
|
||||
|
||||
# If lead detected and not in standstill: always use ACC
|
||||
if self._has_lead_filtered and not (self._standstill_count > 3):
|
||||
self._mode_manager.request_mode('acc', confidence=1.0)
|
||||
return
|
||||
|
||||
# Slow down scenarios: emergency for high urgency, normal for lower urgency
|
||||
if self._has_slow_down:
|
||||
if self._urgency > 0.7:
|
||||
# Emergency: immediate blended mode for high urgency stops
|
||||
self._mode_manager.request_mode('blended', confidence=1.0, emergency=True)
|
||||
else:
|
||||
# Normal: blended with urgency-based confidence
|
||||
confidence = min(1.0, self._urgency * 1.3)
|
||||
self._mode_manager.request_mode('blended', confidence=confidence)
|
||||
return
|
||||
|
||||
# Standstill: use blended
|
||||
if self._standstill_count > 3:
|
||||
self._mode_manager.request_mode('blended', confidence=0.9)
|
||||
return
|
||||
|
||||
# Driving slow: use ACC (but not if actively slowing down)
|
||||
if self._has_slowness and not self._has_slow_down:
|
||||
self._mode_manager.request_mode('acc', confidence=0.8)
|
||||
return
|
||||
|
||||
# Default: ACC
|
||||
self._mode_manager.request_mode('acc', confidence=0.7)
|
||||
self._lead_veto_frames = max(self._lead_veto_frames - 1, 0)
|
||||
return self._lead_veto_frames > 0
|
||||
|
||||
def update(self, sm: messaging.SubMaster) -> None:
|
||||
self._read_params()
|
||||
|
||||
self.set_mpc_fcw_crash_cnt()
|
||||
car_state = sm['carState']
|
||||
md = sm['modelV2']
|
||||
radar_state = sm['radarState']
|
||||
|
||||
self._update_calculations(sm)
|
||||
is_creeping = self._update_creeping(car_state.vEgo)
|
||||
lead_present = radar_state.leadOne.present or radar_state.leadTwo.present
|
||||
self.signals = DecSignals(
|
||||
decel_intent=self._decel_intent(md),
|
||||
curve_detected=self._curve_detected(md),
|
||||
model_trust=self._model_trust(md),
|
||||
creeping=is_creeping and not lead_present,
|
||||
)
|
||||
self.want_blended = should_blend(self.signals)
|
||||
|
||||
if self._CP.radarUnavailable:
|
||||
self._radarless_mode()
|
||||
crash_override = self._mpc.crash_cnt >= 1
|
||||
hard_brake_override = bool(md.meta.hardBrakePredicted)
|
||||
strong_stop = self.signals.model_trust >= MODEL_TRUST_MIN and self.signals.decel_intent >= DECEL_INTENT_CURVE_OVERRIDE
|
||||
raw_lead_veto = self._lead_veto(radar_state, md)
|
||||
urgent_release = (crash_override or hard_brake_override or strong_stop) and not raw_lead_veto
|
||||
self.lead_veto = self._update_lead_veto(raw_lead_veto, lead_present, urgent_release)
|
||||
|
||||
override = (crash_override or hard_brake_override) and not self.lead_veto
|
||||
|
||||
if self._enabled:
|
||||
self._hysteresis.update(self.want_blended, override, self.lead_veto)
|
||||
else:
|
||||
self._radar_mode()
|
||||
self._hysteresis.reset()
|
||||
|
||||
self._mode_manager.update()
|
||||
self._active = sm['selfdriveState'].experimentalMode and self._enabled
|
||||
self._frame += 1
|
||||
|
||||
@@ -1,91 +1,465 @@
|
||||
import numpy as np
|
||||
|
||||
from openpilot.cereal import messaging
|
||||
from opendbc.car import structs
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import (
|
||||
DecSignals,
|
||||
DynamicExperimentalController,
|
||||
ModeHysteresis,
|
||||
should_blend,
|
||||
ENTER_FRAMES,
|
||||
LEAD_VETO_CONFIRM_FRAMES,
|
||||
MIN_BLENDED_FRAMES,
|
||||
)
|
||||
|
||||
class MockLeadOne:
|
||||
def __init__(self, present=0.0):
|
||||
self.present = present
|
||||
T_IDXS = np.array(ModelConstants.T_IDXS)
|
||||
|
||||
class MockRadarState:
|
||||
def __init__(self, present=0.0):
|
||||
self.leadOne = MockLeadOne(present=present)
|
||||
|
||||
class MockCarState:
|
||||
def __init__(self, vEgo=0.0, vCruise=0.0, standstill=False):
|
||||
self.vEgo = vEgo
|
||||
self.vCruise = vCruise
|
||||
self.standstill = standstill
|
||||
|
||||
class MockModelData:
|
||||
def __init__(self, valid=True):
|
||||
size = 33 if valid else 10 # incomplete if invalid
|
||||
self.position = type("Pos", (), {"x": [0.0] * size})()
|
||||
self.orientation = type("Ori", (), {"x": [0.0] * size})()
|
||||
|
||||
class MockSelfDriveState:
|
||||
def __init__(self, experimentalMode=False):
|
||||
self.experimentalMode = experimentalMode
|
||||
|
||||
class MockParams:
|
||||
def __init__(self, enabled=True):
|
||||
self._enabled = enabled
|
||||
|
||||
def get_bool(self, name):
|
||||
return True
|
||||
return self._enabled
|
||||
|
||||
def default_sm():
|
||||
sm = {
|
||||
'carState': MockCarState(vEgo=10.0, vCruise=20.0),
|
||||
'radarState': MockRadarState(present=1.0),
|
||||
'modelV2': MockModelData(valid=True),
|
||||
'selfdriveState': MockSelfDriveState(experimentalMode=True),
|
||||
|
||||
class MockMpc:
|
||||
def __init__(self, crash_cnt=0):
|
||||
self.crash_cnt = crash_cnt
|
||||
|
||||
|
||||
def flat_velocity(v):
|
||||
return [float(v)] * len(T_IDXS)
|
||||
|
||||
|
||||
def decel_velocity(v0, a):
|
||||
return [float(max(0.0, v0 + a * t)) for t in T_IDXS]
|
||||
|
||||
|
||||
def make_car_state(v_ego=10.0, v_cruise=20.0):
|
||||
msg = messaging.new_message('carState')
|
||||
msg.carState.vEgo = v_ego
|
||||
msg.carState.vCruise = v_cruise
|
||||
return msg.carState.as_reader()
|
||||
|
||||
|
||||
def make_selfdrive_state(experimental_mode=True):
|
||||
msg = messaging.new_message('selfdriveState')
|
||||
msg.selfdriveState.experimentalMode = experimental_mode
|
||||
return msg.selfdriveState.as_reader()
|
||||
|
||||
|
||||
def make_radar_state(lead_present=False, lead_radar=False, lead_two_present=False):
|
||||
msg = messaging.new_message('radarState')
|
||||
msg.radarState.leadOne.present = lead_present
|
||||
msg.radarState.leadOne.radar = lead_radar
|
||||
msg.radarState.leadTwo.present = lead_two_present
|
||||
return msg.radarState.as_reader()
|
||||
|
||||
|
||||
def make_model_v2(velocity=None, position_y=None, hard_brake=False, lead_probs=None, frame_drop_perc=0.0):
|
||||
msg = messaging.new_message('modelV2')
|
||||
msg.modelV2.velocity.x = velocity if velocity is not None else flat_velocity(0.0)
|
||||
msg.modelV2.position.y = position_y if position_y is not None else [0.0] * len(T_IDXS)
|
||||
msg.modelV2.frameDropPerc = frame_drop_perc
|
||||
msg.modelV2.meta.hardBrakePredicted = hard_brake
|
||||
if lead_probs is not None:
|
||||
msg.modelV2.init('leadsV3', 3)
|
||||
for i, (prob, prob_time) in enumerate(zip(lead_probs, (0.0, 2.0, 4.0), strict=True)):
|
||||
msg.modelV2.leadsV3[i].prob = prob
|
||||
msg.modelV2.leadsV3[i].probTime = prob_time
|
||||
return msg.modelV2.as_reader()
|
||||
|
||||
|
||||
def make_sm(v_ego=10.0, v_cruise=20.0, velocity=None, position_y=None, hard_brake=False,
|
||||
lead_present=False, lead_radar=False, lead_two_present=False, lead_probs=None,
|
||||
frame_drop_perc=0.0, experimental_mode=True):
|
||||
return {
|
||||
'carState': make_car_state(v_ego, v_cruise),
|
||||
'radarState': make_radar_state(lead_present, lead_radar, lead_two_present),
|
||||
'modelV2': make_model_v2(velocity, position_y, hard_brake, lead_probs, frame_drop_perc),
|
||||
'selfdriveState': make_selfdrive_state(experimental_mode),
|
||||
}
|
||||
return sm
|
||||
|
||||
def mock_cp():
|
||||
class CP:
|
||||
radarUnavailable = False
|
||||
return CP()
|
||||
|
||||
def mock_mpc():
|
||||
class MPC:
|
||||
crash_cnt = 0
|
||||
return MPC()
|
||||
def make_controller(cp=None, mpc=None, enabled=True):
|
||||
return DynamicExperimentalController(cp or structs.CarParams(), mpc or MockMpc(), params=MockParams(enabled))
|
||||
|
||||
# Fake Kalman Filter that always returns a given value
|
||||
class FakeKalman:
|
||||
def __init__(self, value=1.0):
|
||||
self.value = value
|
||||
def add_data(self, v): pass
|
||||
def get_value(self): return self.value
|
||||
def get_confidence(self): return 1.0
|
||||
def reset_data(self): pass
|
||||
|
||||
class TestDynamicExperimentalController(OpenpilotTestCase):
|
||||
def test_initial_mode_is_acc(self, mock_cp, mock_mpc):
|
||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||
def test_initial_mode_is_acc(self):
|
||||
controller = make_controller()
|
||||
assert controller.mode() == "acc"
|
||||
|
||||
def test_standstill_triggers_blended(self, mock_cp, mock_mpc, default_sm):
|
||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||
default_sm['carState'].standstill = True
|
||||
def test_flat_plan_never_blends_at_any_speed(self):
|
||||
for v_ego in (2.5, 5.6, 8.3, 13.9, 22.2, 30.6):
|
||||
controller = make_controller()
|
||||
sm = make_sm(v_ego=v_ego, velocity=flat_velocity(v_ego))
|
||||
for _ in range(100):
|
||||
controller.update(sm)
|
||||
assert controller.mode() == "acc", f"false blend on a flat plan at v_ego={v_ego}"
|
||||
|
||||
def test_highway_slowdown_without_lead_blends(self):
|
||||
v0 = 110 / 3.6
|
||||
a = (70 / 3.6 - v0) / 6.0
|
||||
controller = make_controller()
|
||||
sm = make_sm(v_ego=v0, velocity=decel_velocity(v0, a))
|
||||
for _ in range(10):
|
||||
controller.update(default_sm)
|
||||
controller.update(sm)
|
||||
assert controller.mode() == "blended"
|
||||
|
||||
def test_emergency_blended_on_fcw(self, mock_cp, mock_mpc, default_sm):
|
||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||
mock_mpc.crash_cnt = 1 # simulate FCW
|
||||
for _ in range(2):
|
||||
controller.update(default_sm)
|
||||
def test_curve_exclusion_prevents_false_blend(self):
|
||||
controller = make_controller()
|
||||
sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -1.0), position_y=[6.0] * len(T_IDXS))
|
||||
for _ in range(30):
|
||||
controller.update(sm)
|
||||
assert controller.mode() == "acc"
|
||||
|
||||
def test_curve_does_not_override_saturated_decel_intent(self):
|
||||
controller = make_controller()
|
||||
sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -2.0), position_y=[6.0] * len(T_IDXS))
|
||||
for _ in range(10):
|
||||
controller.update(sm)
|
||||
assert controller.mode() == "blended"
|
||||
|
||||
def test_radarless_slowdown_triggers_blended(self, mock_cp, mock_mpc, default_sm):
|
||||
mock_cp.radarUnavailable = True
|
||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||
|
||||
# Force conditions to simulate slowdown
|
||||
controller._slow_down_filter = FakeKalman(value=1.0) # ty: ignore[invalid-assignment]
|
||||
controller._v_ego_kph = 35.0
|
||||
default_sm['modelV2'] = MockModelData(valid=False) # Incomplete trajectory
|
||||
|
||||
for _ in range(3):
|
||||
controller.update(default_sm)
|
||||
def test_persistent_lead_forces_acc_even_with_strong_model_signal(self):
|
||||
for lead_radar in (True, False):
|
||||
controller = make_controller()
|
||||
sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -2.0),
|
||||
lead_present=True, lead_radar=lead_radar, lead_probs=[1.0, 1.0, 1.0])
|
||||
for _ in range(LEAD_VETO_CONFIRM_FRAMES - 1):
|
||||
controller.update(sm)
|
||||
assert not controller.lead_veto
|
||||
for _ in range(60):
|
||||
controller.update(sm)
|
||||
assert controller.lead_veto
|
||||
assert controller.mode() == "acc"
|
||||
|
||||
def test_single_frame_lead_veto_pulse_does_not_leave_blended(self):
|
||||
controller = make_controller()
|
||||
no_lead_sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -2.0))
|
||||
lead_sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -2.0),
|
||||
lead_present=True, lead_probs=[1.0, 1.0, 1.0])
|
||||
for _ in range(ENTER_FRAMES):
|
||||
controller.update(no_lead_sm)
|
||||
assert controller.mode() == "blended"
|
||||
|
||||
controller.update(lead_sm)
|
||||
assert controller.mode() == "blended"
|
||||
assert not controller.lead_veto
|
||||
|
||||
controller.update(no_lead_sm)
|
||||
assert controller.mode() == "blended"
|
||||
assert not controller.lead_veto
|
||||
|
||||
def test_three_frame_lead_veto_pulse_does_not_leave_blended(self):
|
||||
controller = make_controller()
|
||||
no_lead_sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -2.0))
|
||||
lead_sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -2.0),
|
||||
lead_present=True, lead_probs=[1.0, 1.0, 1.0])
|
||||
for _ in range(ENTER_FRAMES):
|
||||
controller.update(no_lead_sm)
|
||||
assert controller.mode() == "blended"
|
||||
|
||||
for _ in range(LEAD_VETO_CONFIRM_FRAMES - 1):
|
||||
controller.update(lead_sm)
|
||||
assert controller.mode() == "blended"
|
||||
assert not controller.lead_veto
|
||||
|
||||
controller.update(no_lead_sm)
|
||||
assert controller.mode() == "blended"
|
||||
assert not controller.lead_veto
|
||||
|
||||
def test_persistent_lead_veto_forces_acc_after_confirmation(self):
|
||||
controller = make_controller()
|
||||
no_lead_sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -2.0))
|
||||
lead_sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -2.0),
|
||||
lead_present=True, lead_probs=[1.0, 1.0, 1.0])
|
||||
for _ in range(ENTER_FRAMES):
|
||||
controller.update(no_lead_sm)
|
||||
assert controller.mode() == "blended"
|
||||
|
||||
for _ in range(LEAD_VETO_CONFIRM_FRAMES - 1):
|
||||
controller.update(lead_sm)
|
||||
assert controller.mode() == "blended"
|
||||
assert not controller.lead_veto
|
||||
|
||||
controller.update(lead_sm)
|
||||
assert controller.mode() == "acc"
|
||||
assert controller.lead_veto
|
||||
|
||||
controller.update(no_lead_sm)
|
||||
assert controller.mode() == "blended"
|
||||
assert not controller.lead_veto
|
||||
|
||||
def test_veto_releases_without_rebuild_lag(self):
|
||||
controller = make_controller()
|
||||
lead_sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -2.0),
|
||||
lead_present=True, lead_probs=[1.0, 1.0, 1.0])
|
||||
for _ in range(30):
|
||||
controller.update(lead_sm)
|
||||
assert controller.mode() == "acc"
|
||||
assert controller.lead_veto
|
||||
|
||||
no_lead_sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -2.0), lead_present=False)
|
||||
for _ in range(ENTER_FRAMES + 2):
|
||||
controller.update(no_lead_sm)
|
||||
if controller.mode() == "blended":
|
||||
break
|
||||
assert controller.mode() == "blended"
|
||||
|
||||
def test_lead_gone_with_no_underlying_slowdown_stays_acc(self):
|
||||
controller = make_controller()
|
||||
lead_sm = make_sm(v_ego=20.0, velocity=flat_velocity(20.0), lead_present=True, lead_probs=[1.0, 1.0, 1.0])
|
||||
for _ in range(30):
|
||||
controller.update(lead_sm)
|
||||
assert controller.mode() == "acc"
|
||||
|
||||
no_lead_sm = make_sm(v_ego=20.0, velocity=flat_velocity(20.0), lead_present=False)
|
||||
for _ in range(20):
|
||||
controller.update(no_lead_sm)
|
||||
assert controller.mode() == "acc"
|
||||
|
||||
def test_creep_does_not_release_lead_veto(self):
|
||||
controller = make_controller()
|
||||
sm = make_sm(v_ego=1.0, velocity=flat_velocity(1.0), lead_present=True, lead_probs=[1.0, 1.0, 1.0])
|
||||
for _ in range(10):
|
||||
controller.update(sm)
|
||||
assert controller.mode() == "acc"
|
||||
assert controller.lead_veto
|
||||
|
||||
def test_lead_prevents_creep_only_blending_when_model_probability_drops(self):
|
||||
controller = make_controller()
|
||||
confirmed_sm = make_sm(v_ego=1.0, velocity=flat_velocity(1.0), lead_present=True, lead_probs=[1.0, 1.0, 1.0])
|
||||
for _ in range(LEAD_VETO_CONFIRM_FRAMES):
|
||||
controller.update(confirmed_sm)
|
||||
assert controller.lead_veto
|
||||
|
||||
low_probability_sm = make_sm(v_ego=1.0, velocity=flat_velocity(1.0), lead_present=True, lead_probs=[1.0, 0.1, 0.1])
|
||||
for _ in range(LEAD_VETO_CONFIRM_FRAMES - 1):
|
||||
controller.update(low_probability_sm)
|
||||
assert controller.lead_veto
|
||||
assert controller.mode() == "acc"
|
||||
|
||||
for _ in range(20):
|
||||
controller.update(low_probability_sm)
|
||||
assert not controller.lead_veto
|
||||
assert not controller.signals.creeping
|
||||
assert controller.mode() == "acc"
|
||||
|
||||
no_lead_sm = make_sm(v_ego=1.0, velocity=flat_velocity(1.0), lead_present=False, lead_probs=[1.0, 0.1, 0.1])
|
||||
for _ in range(ENTER_FRAMES):
|
||||
controller.update(no_lead_sm)
|
||||
assert controller.mode() == "blended"
|
||||
|
||||
def test_confirmed_lead_veto_ignores_short_future_probability_dropout(self):
|
||||
controller = make_controller()
|
||||
confirmed_sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -1.0),
|
||||
lead_present=True, lead_probs=[1.0, 1.0, 1.0])
|
||||
for _ in range(LEAD_VETO_CONFIRM_FRAMES):
|
||||
controller.update(confirmed_sm)
|
||||
assert controller.lead_veto
|
||||
|
||||
dropout_sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -1.0),
|
||||
lead_present=True, lead_probs=[1.0, 0.1, 0.1])
|
||||
for _ in range(LEAD_VETO_CONFIRM_FRAMES - 1):
|
||||
controller.update(dropout_sm)
|
||||
assert controller.lead_veto
|
||||
assert controller.mode() == "acc"
|
||||
|
||||
controller.update(confirmed_sm)
|
||||
assert controller.lead_veto
|
||||
assert controller.mode() == "acc"
|
||||
|
||||
def test_urgent_override_bypasses_confirmed_veto_release(self):
|
||||
controller = make_controller()
|
||||
confirmed_sm = make_sm(v_ego=20.0, velocity=flat_velocity(20.0),
|
||||
lead_present=True, lead_probs=[1.0, 1.0, 1.0])
|
||||
for _ in range(LEAD_VETO_CONFIRM_FRAMES):
|
||||
controller.update(confirmed_sm)
|
||||
assert controller.lead_veto
|
||||
|
||||
hard_brake_sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -2.0), hard_brake=True,
|
||||
lead_present=True, lead_probs=[1.0, 0.1, 0.1])
|
||||
controller.update(hard_brake_sm)
|
||||
assert not controller.lead_veto
|
||||
assert controller.mode() == "blended"
|
||||
|
||||
def test_trusted_strong_stop_bypasses_confirmed_veto_release(self):
|
||||
controller = make_controller()
|
||||
confirmed_sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -2.0),
|
||||
lead_present=True, lead_probs=[1.0, 1.0, 1.0])
|
||||
for _ in range(LEAD_VETO_CONFIRM_FRAMES):
|
||||
controller.update(confirmed_sm)
|
||||
assert controller.lead_veto
|
||||
assert controller.mode() == "acc"
|
||||
|
||||
departing_lead_sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -2.0),
|
||||
lead_present=True, lead_probs=[1.0, 0.1, 0.1])
|
||||
controller.update(departing_lead_sm)
|
||||
assert not controller.lead_veto
|
||||
assert controller.mode() == "blended"
|
||||
|
||||
def test_degraded_strong_stop_does_not_bypass_confirmed_veto_release(self):
|
||||
controller = make_controller()
|
||||
confirmed_sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -2.0),
|
||||
lead_present=True, lead_probs=[1.0, 1.0, 1.0])
|
||||
for _ in range(LEAD_VETO_CONFIRM_FRAMES):
|
||||
controller.update(confirmed_sm)
|
||||
assert controller.lead_veto
|
||||
|
||||
degraded_sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -2.0), lead_present=True,
|
||||
lead_probs=[1.0, 0.1, 0.1], frame_drop_perc=60.0)
|
||||
for _ in range(LEAD_VETO_CONFIRM_FRAMES - 1):
|
||||
controller.update(degraded_sm)
|
||||
assert controller.lead_veto
|
||||
assert controller.mode() == "acc"
|
||||
|
||||
def test_model_slowdown_still_blends_while_creeping_with_a_lead(self):
|
||||
controller = make_controller()
|
||||
sm = make_sm(v_ego=1.0, velocity=decel_velocity(1.0, -1.0), lead_present=True, lead_probs=[1.0, 0.1, 0.1])
|
||||
for _ in range(ENTER_FRAMES):
|
||||
controller.update(sm)
|
||||
assert not controller.lead_veto
|
||||
assert controller.mode() == "blended"
|
||||
|
||||
def test_hard_brake_still_blends_while_creeping_with_a_lead(self):
|
||||
controller = make_controller()
|
||||
sm = make_sm(v_ego=1.0, velocity=flat_velocity(1.0), hard_brake=True,
|
||||
lead_present=True, lead_probs=[1.0, 0.1, 0.1])
|
||||
controller.update(sm)
|
||||
assert not controller.lead_veto
|
||||
assert controller.mode() == "blended"
|
||||
|
||||
def test_creep_hysteresis_band_without_lead(self):
|
||||
controller = make_controller()
|
||||
controller.update(make_sm(v_ego=1.5, velocity=flat_velocity(1.5)))
|
||||
assert controller.signals.creeping
|
||||
|
||||
controller.update(make_sm(v_ego=2.5, velocity=flat_velocity(2.5)))
|
||||
assert controller.signals.creeping, "a small excursion above CREEP_SPEED_ENTER should not exit creeping"
|
||||
|
||||
controller.update(make_sm(v_ego=5.0, velocity=flat_velocity(5.0)))
|
||||
assert not controller.signals.creeping, "should exit creeping once genuinely above CREEP_SPEED_EXIT"
|
||||
|
||||
def test_crash_cnt_override_inert_while_lead_present(self):
|
||||
mpc = MockMpc(crash_cnt=0)
|
||||
controller = make_controller(mpc=mpc)
|
||||
sm = make_sm(v_ego=20.0, velocity=flat_velocity(20.0), lead_present=True, lead_probs=[1.0, 1.0, 1.0])
|
||||
for _ in range(30):
|
||||
controller.update(sm)
|
||||
assert controller.mode() == "acc"
|
||||
|
||||
mpc.crash_cnt = 1
|
||||
controller.update(sm)
|
||||
assert controller.mode() == "acc"
|
||||
|
||||
def test_crash_cnt_blends_within_one_frame_without_lead(self):
|
||||
mpc = MockMpc(crash_cnt=1)
|
||||
controller = make_controller(mpc=mpc)
|
||||
sm = make_sm(v_ego=20.0, velocity=flat_velocity(20.0), lead_present=False)
|
||||
controller.update(sm)
|
||||
assert controller.mode() == "blended"
|
||||
|
||||
def test_hard_brake_predicted_blends_within_one_frame_without_lead(self):
|
||||
controller = make_controller()
|
||||
sm = make_sm(v_ego=20.0, velocity=flat_velocity(20.0), hard_brake=True, lead_present=False)
|
||||
controller.update(sm)
|
||||
assert controller.mode() == "blended"
|
||||
|
||||
def test_confirmed_lead_veto_suppresses_hard_brake_override(self):
|
||||
controller = make_controller()
|
||||
sm = make_sm(v_ego=20.0, velocity=flat_velocity(20.0), hard_brake=True,
|
||||
lead_present=True, lead_probs=[1.0, 1.0, 1.0])
|
||||
for _ in range(LEAD_VETO_CONFIRM_FRAMES - 1):
|
||||
controller.update(sm)
|
||||
assert controller.mode() == "blended"
|
||||
assert not controller.lead_veto
|
||||
controller.update(sm)
|
||||
assert controller.mode() == "acc"
|
||||
assert controller.lead_veto
|
||||
|
||||
def test_degraded_model_does_not_blend(self):
|
||||
controller = make_controller()
|
||||
sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -3.0), frame_drop_perc=60.0)
|
||||
for _ in range(30):
|
||||
controller.update(sm)
|
||||
assert controller.mode() == "acc"
|
||||
|
||||
def test_short_plan_arrays_do_not_blend(self):
|
||||
controller = make_controller()
|
||||
sm = make_sm(v_ego=20.0, velocity=[20.0] * 5)
|
||||
for _ in range(30):
|
||||
controller.update(sm)
|
||||
assert controller.mode() == "acc"
|
||||
|
||||
def test_disabled_param_holds_acc(self):
|
||||
controller = make_controller(enabled=False)
|
||||
sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -3.0))
|
||||
for _ in range(30):
|
||||
controller.update(sm)
|
||||
assert controller.mode() == "acc"
|
||||
|
||||
|
||||
class TestModeHysteresis(OpenpilotTestCase):
|
||||
def test_entry_requires_enter_frames(self):
|
||||
h = ModeHysteresis()
|
||||
for _ in range(ENTER_FRAMES - 1):
|
||||
assert h.update(want_blended=True, override=False, veto=False) == "acc"
|
||||
assert h.update(want_blended=True, override=False, veto=False) == "blended"
|
||||
|
||||
def test_override_beats_veto(self):
|
||||
h = ModeHysteresis()
|
||||
assert h.update(want_blended=False, override=True, veto=True) == "blended"
|
||||
|
||||
def test_veto_forces_acc_even_when_reason_active(self):
|
||||
h = ModeHysteresis()
|
||||
for _ in range(ENTER_FRAMES + 5):
|
||||
assert h.update(want_blended=True, override=False, veto=True) == "acc"
|
||||
|
||||
def test_counter_accumulates_under_veto_then_releases_instantly(self):
|
||||
h = ModeHysteresis()
|
||||
for _ in range(ENTER_FRAMES + 5):
|
||||
h.update(want_blended=True, override=False, veto=True)
|
||||
assert h.mode == "acc"
|
||||
assert h.update(want_blended=True, override=False, veto=False) == "blended"
|
||||
|
||||
def test_exit_requires_min_dwell_and_sustained_absence(self):
|
||||
h = ModeHysteresis()
|
||||
for _ in range(ENTER_FRAMES):
|
||||
h.update(want_blended=True, override=False, veto=False)
|
||||
assert h.mode == "blended"
|
||||
for _ in range(MIN_BLENDED_FRAMES - 1):
|
||||
assert h.update(want_blended=False, override=False, veto=False) == "blended"
|
||||
assert h.update(want_blended=False, override=False, veto=False) == "acc"
|
||||
|
||||
def test_no_flapping_on_alternating_reason(self):
|
||||
h = ModeHysteresis()
|
||||
changes = 0
|
||||
prev = h.mode
|
||||
for i in range(200):
|
||||
mode = h.update(want_blended=i % 2 == 0, override=False, veto=False)
|
||||
changes += mode != prev
|
||||
prev = mode
|
||||
assert changes == 0
|
||||
|
||||
|
||||
class TestShouldBlend(OpenpilotTestCase):
|
||||
def test_slowdown_detected_triggers(self):
|
||||
assert should_blend(DecSignals(decel_intent=1.0))
|
||||
assert not should_blend(DecSignals(decel_intent=0.0))
|
||||
|
||||
def test_curve_exclusion_suppresses_slowdown(self):
|
||||
assert not should_blend(DecSignals(decel_intent=0.7, curve_detected=True))
|
||||
|
||||
def test_curve_exclusion_does_not_override_saturated_decel_intent(self):
|
||||
assert should_blend(DecSignals(decel_intent=1.0, curve_detected=True))
|
||||
|
||||
def test_degraded_model_suppresses_model_based_reasons(self):
|
||||
s = DecSignals(decel_intent=1.0, model_trust=0.0)
|
||||
assert not should_blend(s)
|
||||
|
||||
def test_creep_bypasses_everything(self):
|
||||
assert should_blend(DecSignals(model_trust=0.0, creeping=True))
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
STOPPING_DISTANCE = 1.5
|
||||
STOPPED_SPEED = 0.02
|
||||
STOPPING_TIME = 2.5
|
||||
MIN_STOPPING_HOLD_ACCEL = -0.2
|
||||
STOPPING_DECEL_RATE = 0.3
|
||||
STOPPED_DECEL_RATE = 0.05
|
||||
|
||||
|
||||
class LongControlSP:
|
||||
def should_hold_stopping(self, CS, a_target: float) -> bool:
|
||||
return (self.last_output_accel <= MIN_STOPPING_HOLD_ACCEL and a_target >= self.last_output_accel and CS.vEgo > STOPPED_SPEED and CS.aEgo < 0.0
|
||||
and CS.vEgo <= -CS.aEgo * STOPPING_TIME and CS.vEgo ** 2 <= -2.0 * CS.aEgo * STOPPING_DISTANCE)
|
||||
|
||||
@staticmethod
|
||||
def stopping_decel_rate(v_ego: float) -> float:
|
||||
return float(np.interp(v_ego, [0.0, 0.5, 1.5], [STOPPED_DECEL_RATE, 0.15, STOPPING_DECEL_RATE]))
|
||||
@@ -5,10 +5,13 @@ This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
from openpilot.cereal import messaging, custom
|
||||
import math
|
||||
|
||||
from openpilot.cereal import messaging, custom, log
|
||||
from opendbc.car import structs
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.selfdrive.car.cruise import V_CRUISE_MAX
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.accel_controller.accel_controller import AccelController
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.e2e_alerts_helper import E2EAlertsHelper
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.smart_cruise_control import SmartCruiseControl
|
||||
@@ -19,12 +22,17 @@ from openpilot.sunnypilot.models.helpers import get_active_bundle
|
||||
|
||||
DecState = custom.LongitudinalPlanSP.DynamicExperimentalControl.DynamicExperimentalControlState
|
||||
LongitudinalPlanSource = custom.LongitudinalPlanSP.LongitudinalPlanSource
|
||||
MpcPlanSource = log.LongitudinalPlan.LongitudinalPlanSource
|
||||
|
||||
E2E_BRAKE_HOLD_ACCEL = -0.2 # m/s^2
|
||||
|
||||
|
||||
class LongitudinalPlannerSP:
|
||||
def __init__(self, CP: structs.CarParams, CP_SP: structs.CarParamsSP, mpc):
|
||||
self.accel_controller = AccelController()
|
||||
self.accel_controller_active = False
|
||||
self.lead_one = None
|
||||
self.events_sp = EventsSP()
|
||||
self.resolver = SpeedLimitResolver()
|
||||
self.dec = DynamicExperimentalController(CP, mpc)
|
||||
self.scc = SmartCruiseControl()
|
||||
self.resolver = SpeedLimitResolver()
|
||||
@@ -38,10 +46,49 @@ class LongitudinalPlannerSP:
|
||||
|
||||
def is_e2e(self, sm: messaging.SubMaster) -> bool:
|
||||
experimental_mode = sm['selfdriveState'].experimentalMode
|
||||
if not self.dec.active():
|
||||
return experimental_mode
|
||||
if not experimental_mode:
|
||||
return False
|
||||
|
||||
return experimental_mode and self.dec.mode() == "blended"
|
||||
if not self.dec.active() or self.dec.mode() == "blended":
|
||||
return True
|
||||
|
||||
if self.mpc.source == MpcPlanSource.e2e and sm['modelV2'].action.desiredAcceleration < E2E_BRAKE_HOLD_ACCEL:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_max_accel_override(self, v_ego: float, engine_off: bool = False, lead=None) -> float | None:
|
||||
if not self.accel_controller.is_enabled():
|
||||
return None
|
||||
|
||||
# the eco lead pull-away boost needs the lead; the stock planner does not pass it, so update() keeps it
|
||||
return self.accel_controller.get_max_accel(v_ego, engine_off, lead if lead is not None else self.lead_one)
|
||||
|
||||
def get_cruise_target_override(self, v_ego: float, v_target: float, force_decel: bool, accel_coast: float | None = None) -> float:
|
||||
if not self.accel_controller.is_enabled() or force_decel or self.source != LongitudinalPlanSource.cruise:
|
||||
return v_target
|
||||
|
||||
return self.accel_controller.get_cruise_target(v_ego, v_target, accel_coast)
|
||||
|
||||
def is_accel_controller_active(self, force_decel: bool) -> bool:
|
||||
return bool(self.accel_controller.is_enabled() and not force_decel and
|
||||
self.mpc.source == MpcPlanSource.cruise)
|
||||
|
||||
def _has_valid_selected_lead(self, sm: messaging.SubMaster, source: MpcPlanSource) -> bool:
|
||||
radar_valid = sm.valid.get('radarState', False) and getattr(sm, 'alive', {}).get('radarState', False)
|
||||
return radar_valid and ((source == MpcPlanSource.lead0 and sm['radarState'].leadOne.present) or
|
||||
(source == MpcPlanSource.lead1 and sm['radarState'].leadTwo.present))
|
||||
|
||||
def arbitrate_cruise_candidate(self, sm: messaging.SubMaster, gated: float, ungated: float,
|
||||
mpc_accel: float, mpc_source: MpcPlanSource, *, allow_throttle: bool,
|
||||
e2e: bool, force_decel: bool) -> float:
|
||||
finite = all(math.isfinite(value) for value in (gated, ungated, mpc_accel))
|
||||
coast_gate_changed_source = gated < mpc_accel <= ungated
|
||||
if (finite and not allow_throttle and not e2e and not force_decel
|
||||
and self._has_valid_selected_lead(sm, mpc_source) and coast_gate_changed_source):
|
||||
return ungated
|
||||
|
||||
return gated
|
||||
|
||||
def update_targets(self, sm: messaging.SubMaster, v_ego: float, a_ego: float, v_cruise: float) -> tuple[float, float]:
|
||||
CS = sm['carState']
|
||||
@@ -74,10 +121,14 @@ class LongitudinalPlannerSP:
|
||||
return self.output_v_target, self.output_a_target
|
||||
|
||||
def update(self, sm: messaging.SubMaster) -> None:
|
||||
self.accel_controller.update()
|
||||
self.lead_one = sm['radarState'].leadOne
|
||||
self.events_sp.clear()
|
||||
self.dec.update(sm)
|
||||
self.e2e_alerts_helper.update(sm, self.events_sp)
|
||||
|
||||
def update_dec(self, sm: messaging.SubMaster) -> None:
|
||||
self.dec.update(sm)
|
||||
|
||||
def publish_longitudinal_plan_sp(self, sm: messaging.SubMaster, pm: messaging.PubMaster) -> None:
|
||||
plan_sp_send = messaging.new_message('longitudinalPlanSP')
|
||||
|
||||
@@ -94,6 +145,15 @@ class LongitudinalPlannerSP:
|
||||
dec.state = DecState.blended if self.dec.mode() == 'blended' else DecState.acc
|
||||
dec.enabled = self.dec.enabled()
|
||||
dec.active = self.dec.active()
|
||||
dec.decelIntent = float(self.dec.signals.decel_intent)
|
||||
dec.curveDetected = bool(self.dec.signals.curve_detected)
|
||||
dec.wantBlended = bool(self.dec.want_blended)
|
||||
dec.leadVeto = bool(self.dec.lead_veto)
|
||||
|
||||
accel_controller = longitudinalPlanSP.accelController
|
||||
accel_controller.enabled = bool(self.accel_controller.is_enabled())
|
||||
accel_controller.active = bool(self.accel_controller_active)
|
||||
accel_controller.profile = int(self.accel_controller.profile)
|
||||
|
||||
# Smart Cruise Control
|
||||
smartCruiseControl = longitudinalPlanSP.smartCruiseControl
|
||||
|
||||
+368
-11
@@ -4,6 +4,8 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
@@ -15,8 +17,23 @@ from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlannerSP, LongitudinalPlanSource
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control import MIN_V
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.vision_controller import SmartCruiseControlVision, _ENTERING_PRED_LAT_ACC_TH
|
||||
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.vision_controller import (
|
||||
_A_LAT_REG_MAX,
|
||||
_BELOW_EGO_TARGET_RELEASE_RATE,
|
||||
_ENTERING_PRED_LAT_ACC_TH,
|
||||
_MIN_ACTIVATION_SPEED,
|
||||
_RELIEF_CONFIRMATION_FRAMES,
|
||||
_TARGET_RELEASE_CONFIRMATION_FRAMES,
|
||||
_TARGET_RELEASE_RATE,
|
||||
_TARGET_TIGHTEN_CONFIRMATION_FRAMES,
|
||||
_TARGET_TIGHTEN_RATE,
|
||||
_TURNING_LAT_ACC_TH,
|
||||
_URGENT_PRED_LAT_ACC_TH,
|
||||
SmartCruiseControlVision,
|
||||
)
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
|
||||
VisionState = custom.LongitudinalPlanSP.SmartCruiseControl.VisionState
|
||||
@@ -107,7 +124,6 @@ def generate_controlsState():
|
||||
|
||||
|
||||
class TestSmartCruiseControlVision(OpenpilotTestCase):
|
||||
|
||||
def setup_method(self):
|
||||
self.params = Params()
|
||||
self.reset_params()
|
||||
@@ -121,36 +137,377 @@ class TestSmartCruiseControlVision(OpenpilotTestCase):
|
||||
def reset_params(self):
|
||||
self.params.put_bool("SmartCruiseControlVision", True, block=True)
|
||||
|
||||
def assert_approx(self, actual, expected):
|
||||
self.assertAlmostEqual(actual, expected, delta=max(1e-12, abs(expected) * 1e-6))
|
||||
|
||||
def set_lat_accels(self, current: float, predicted: float, v_ego: float = 20.0, model_speed: float = 20.0) -> None:
|
||||
self.sm['controlsState'].curvature = current / v_ego**2
|
||||
self.sm['modelV2'].velocity.x = [model_speed] * len(ModelConstants.T_IDXS)
|
||||
self.sm['modelV2'].orientationRate.z = [predicted / model_speed] * len(ModelConstants.T_IDXS)
|
||||
|
||||
def update_lat_accels(
|
||||
self, current: float, predicted: float, cruise: float = 30.0, a_ego: float = 0.0, v_ego: float = 20.0, model_speed: float = 20.0
|
||||
) -> None:
|
||||
self.set_lat_accels(current, predicted, v_ego, model_speed)
|
||||
self.scc_v.update(self.sm, True, False, v_ego, a_ego, cruise)
|
||||
|
||||
def enter_curve(self, predicted: float = 2.2) -> None:
|
||||
self.update_lat_accels(0.5, predicted)
|
||||
self.update_lat_accels(0.5, predicted)
|
||||
assert self.scc_v.state == VisionState.entering
|
||||
|
||||
def test_initial_state(self):
|
||||
assert self.scc_v.state == VisionState.disabled
|
||||
assert not self.scc_v.is_active
|
||||
assert self.scc_v.output_v_target == V_CRUISE_UNSET
|
||||
assert self.scc_v.output_a_target == 0.
|
||||
assert self.scc_v.output_a_target == 0.0
|
||||
|
||||
def test_system_disabled(self):
|
||||
self.params.put_bool("SmartCruiseControlVision", False, block=True)
|
||||
self.scc_v.enabled = self.params.get_bool("SmartCruiseControlVision")
|
||||
|
||||
for _ in range(int(10. / DT_MDL)):
|
||||
self.scc_v.update(self.sm, True, False, 0., 0., 0.)
|
||||
for _ in range(int(10.0 / DT_MDL)):
|
||||
self.scc_v.update(self.sm, True, False, 0.0, 0.0, 0.0)
|
||||
assert self.scc_v.state == VisionState.disabled
|
||||
assert not self.scc_v.is_active
|
||||
|
||||
def test_disabled(self):
|
||||
for _ in range(int(10. / DT_MDL)):
|
||||
self.scc_v.update(self.sm, False, False, 0., 0., 0.)
|
||||
for _ in range(int(10.0 / DT_MDL)):
|
||||
self.scc_v.update(self.sm, False, False, 0.0, 0.0, 0.0)
|
||||
assert self.scc_v.state == VisionState.disabled
|
||||
|
||||
def test_transition_disabled_to_enabled(self):
|
||||
for _ in range(int(10. / DT_MDL)):
|
||||
self.scc_v.update(self.sm, True, False, 0., 0., 0.)
|
||||
for _ in range(int(10.0 / DT_MDL)):
|
||||
self.scc_v.update(self.sm, True, False, 0.0, 0.0, 0.0)
|
||||
assert self.scc_v.state == VisionState.enabled
|
||||
|
||||
@parameterized.expand([
|
||||
def test_unconfirmed_release_holds_but_urgent_reentry_tightens(self):
|
||||
self.enter_curve()
|
||||
targets = [self.scc_v.output_v_target]
|
||||
|
||||
self.update_lat_accels(2.0, 2.2, a_ego=-0.8)
|
||||
assert self.scc_v.state == VisionState.turning
|
||||
assert self.scc_v.output_a_target == -0.8
|
||||
turning_demand = self.scc_v._v_demand()
|
||||
targets.append(self.scc_v.output_v_target)
|
||||
|
||||
self.update_lat_accels(1.2, 1.2, a_ego=0.3)
|
||||
assert self.scc_v.state == VisionState.leaving
|
||||
assert self.scc_v.output_a_target == 0.3
|
||||
targets.append(self.scc_v.output_v_target)
|
||||
|
||||
self.update_lat_accels(1.0, 3.0, a_ego=-1.2)
|
||||
assert self.scc_v.state == VisionState.entering
|
||||
assert self.scc_v.output_a_target == -1.2
|
||||
reentry_demand = self.scc_v._v_demand()
|
||||
targets.append(self.scc_v.output_v_target)
|
||||
|
||||
entering, turning, leaving, reentering = targets
|
||||
assert turning < entering
|
||||
self.assert_approx(turning, turning_demand)
|
||||
self.assert_approx(leaving, turning)
|
||||
assert reentering < leaving
|
||||
self.assert_approx(reentering, reentry_demand)
|
||||
|
||||
def test_new_curve_interrupts_confirmed_release_immediately(self):
|
||||
self.enter_curve()
|
||||
for _ in range(_RELIEF_CONFIRMATION_FRAMES + 1):
|
||||
self.update_lat_accels(0.8, 0.8)
|
||||
releasing_v_target = self.scc_v.output_v_target
|
||||
assert self.scc_v.state == VisionState.leaving
|
||||
|
||||
self.update_lat_accels(0.8, 3.0, a_ego=-0.7)
|
||||
assert self.scc_v.state == VisionState.entering
|
||||
assert self.scc_v.output_v_target < releasing_v_target
|
||||
assert self.scc_v.output_a_target == -0.7
|
||||
|
||||
@parameterized.expand([(-2.0,), (-0.5,), (0.0,), (0.8,)])
|
||||
def test_planner_acceleration_passes_through_exactly(self, planner_accel):
|
||||
self.enter_curve()
|
||||
self.update_lat_accels(0.5, 2.2, a_ego=planner_accel)
|
||||
assert self.scc_v.output_a_target == planner_accel
|
||||
|
||||
def test_planner_acceleration_passes_through_all_states(self):
|
||||
cases = (
|
||||
(False, False, 0.5, 2.2, -0.2, VisionState.disabled),
|
||||
(True, False, 0.5, 0.8, 0.1, VisionState.enabled),
|
||||
(True, False, 0.5, 2.2, -0.4, VisionState.entering),
|
||||
(True, False, 2.0, 2.2, -0.8, VisionState.turning),
|
||||
(True, False, 1.2, 1.2, 0.3, VisionState.leaving),
|
||||
(True, True, 1.2, 1.2, 0.6, VisionState.overriding),
|
||||
)
|
||||
for long_enabled, override, current, predicted, planner_accel, state in cases:
|
||||
self.set_lat_accels(current, predicted)
|
||||
self.scc_v.update(self.sm, long_enabled, override, 20.0, planner_accel, 30.0)
|
||||
assert self.scc_v.state == state
|
||||
assert self.scc_v.output_a_target == planner_accel
|
||||
|
||||
def test_jitter_requires_confirmed_relief_then_releases_smoothly(self):
|
||||
self.enter_curve()
|
||||
previous_v_target = self.scc_v.output_v_target
|
||||
|
||||
for frame in range(_RELIEF_CONFIRMATION_FRAMES * 2):
|
||||
self.update_lat_accels(1.0, 1.05 if frame % 2 == 0 else 1.15)
|
||||
assert self.scc_v.state == VisionState.entering
|
||||
assert self.scc_v.output_v_target >= previous_v_target
|
||||
assert self.scc_v.output_v_target - previous_v_target <= _BELOW_EGO_TARGET_RELEASE_RATE * DT_MDL + 1e-9
|
||||
previous_v_target = self.scc_v.output_v_target
|
||||
|
||||
for _ in range(_RELIEF_CONFIRMATION_FRAMES):
|
||||
self.update_lat_accels(1.15, 0.8)
|
||||
assert self.scc_v.state == VisionState.entering
|
||||
assert 0.0 <= self.scc_v.output_v_target - previous_v_target <= _BELOW_EGO_TARGET_RELEASE_RATE * DT_MDL + 1e-9
|
||||
previous_v_target = self.scc_v.output_v_target
|
||||
|
||||
release_cruise = 30.0
|
||||
for _ in range(_RELIEF_CONFIRMATION_FRAMES - 1):
|
||||
self.update_lat_accels(0.8, 0.8, release_cruise)
|
||||
assert self.scc_v.state == VisionState.entering
|
||||
assert 0.0 <= self.scc_v.output_v_target - previous_v_target <= _BELOW_EGO_TARGET_RELEASE_RATE * DT_MDL + 1e-9
|
||||
previous_v_target = self.scc_v.output_v_target
|
||||
|
||||
active_v_targets = [previous_v_target]
|
||||
for _ in range(int((release_cruise - previous_v_target) / (_TARGET_RELEASE_RATE * DT_MDL)) + 10):
|
||||
self.update_lat_accels(0.8, 0.8, release_cruise)
|
||||
if not self.scc_v.is_active:
|
||||
break
|
||||
assert self.scc_v.state == VisionState.leaving
|
||||
assert self.scc_v.output_v_target != V_CRUISE_UNSET
|
||||
active_v_targets.append(self.scc_v.output_v_target)
|
||||
|
||||
assert self.scc_v.state == VisionState.enabled
|
||||
assert self.scc_v.output_v_target == V_CRUISE_UNSET
|
||||
self.assert_approx(active_v_targets[-1], release_cruise)
|
||||
assert np.all((np.diff(active_v_targets) >= 0.0) & (np.diff(active_v_targets) <= _BELOW_EGO_TARGET_RELEASE_RATE * DT_MDL + 1e-9))
|
||||
|
||||
def test_target_release_waits_for_relief_above_ego_speed(self):
|
||||
self.enter_curve()
|
||||
held_v_target = self.scc_v.output_v_target
|
||||
self.assert_approx(held_v_target, self.scc_v.v_ego)
|
||||
|
||||
for _ in range(_RELIEF_CONFIRMATION_FRAMES + _TARGET_RELEASE_CONFIRMATION_FRAMES - 2):
|
||||
self.update_lat_accels(0.8, 0.8)
|
||||
self.assert_approx(self.scc_v.output_v_target, held_v_target)
|
||||
|
||||
self.update_lat_accels(0.8, 0.8)
|
||||
rise = self.scc_v.output_v_target - held_v_target
|
||||
assert 0.0 < rise <= _TARGET_RELEASE_RATE * DT_MDL + 1e-9
|
||||
|
||||
def test_curve_target_is_independent_of_ego_speed(self):
|
||||
model_speed = 24.0
|
||||
predicted_yaw_rate = 0.12
|
||||
predicted_lat_accel = model_speed * predicted_yaw_rate
|
||||
expected_v_target = (_A_LAT_REG_MAX / (predicted_yaw_rate / model_speed)) ** 0.5
|
||||
targets = []
|
||||
|
||||
for v_ego in (18.0, 28.0):
|
||||
controller = SmartCruiseControlVision()
|
||||
self.set_lat_accels(0.5, predicted_lat_accel, v_ego, model_speed)
|
||||
controller.update(self.sm, True, False, v_ego, 0.0, 30.0)
|
||||
controller.update(self.sm, True, False, v_ego, 0.0, 30.0)
|
||||
assert controller.state == VisionState.entering
|
||||
targets.append(controller.v_target)
|
||||
|
||||
self.assert_approx(targets[0], expected_v_target)
|
||||
self.assert_approx(targets[1], expected_v_target)
|
||||
|
||||
def test_curve_target_respects_minimum_speed_floor(self):
|
||||
model_speed = 10.0
|
||||
predicted_yaw_rate = 2.0
|
||||
self.set_lat_accels(0.5, model_speed * predicted_yaw_rate, model_speed=model_speed)
|
||||
self.scc_v.update(self.sm, True, False, 20.0, 0.0, 30.0)
|
||||
self.scc_v.update(self.sm, True, False, 20.0, 0.0, 30.0)
|
||||
|
||||
assert self.scc_v.state == VisionState.entering
|
||||
assert self.scc_v.v_target < MIN_V
|
||||
self.assert_approx(self.scc_v.output_v_target, MIN_V)
|
||||
|
||||
@parameterized.expand(
|
||||
[([], []), ([np.nan] * len(ModelConstants.T_IDXS), [np.nan] * len(ModelConstants.T_IDXS)), ([20.0] * 5, [0.1] * 3)],
|
||||
names=["velocities", "yaw_rates"],
|
||||
)
|
||||
def test_model_vector_edges_remain_finite(self, velocities, yaw_rates):
|
||||
self.sm['modelV2'].velocity.x = velocities
|
||||
self.sm['modelV2'].orientationRate.z = yaw_rates
|
||||
self.scc_v.update(self.sm, True, False, 20.0, 0.0, 30.0)
|
||||
self.scc_v.update(self.sm, True, False, 20.0, 0.0, 30.0)
|
||||
|
||||
assert all(
|
||||
np.isfinite(value)
|
||||
for value in (
|
||||
self.scc_v.current_lat_acc,
|
||||
self.scc_v.max_pred_lat_acc,
|
||||
self.scc_v.v_target,
|
||||
self.scc_v.output_v_target,
|
||||
self.scc_v.output_a_target,
|
||||
)
|
||||
)
|
||||
|
||||
@parameterized.expand([(5.75,), (9.9,), (_MIN_ACTIVATION_SPEED,)])
|
||||
def test_vision_control_does_not_steal_launch(self, launch_speed):
|
||||
self.set_lat_accels(0.5, 3.0, launch_speed)
|
||||
self.scc_v.update(self.sm, True, False, launch_speed, 0.0, 30.0)
|
||||
self.scc_v.update(self.sm, True, False, launch_speed, 0.0, 30.0)
|
||||
|
||||
assert launch_speed <= _MIN_ACTIVATION_SPEED
|
||||
assert self.scc_v.state == VisionState.enabled
|
||||
assert not self.scc_v.is_active
|
||||
assert self.scc_v.output_v_target == V_CRUISE_UNSET
|
||||
|
||||
def test_vision_control_can_activate_above_launch_range(self):
|
||||
speed = _MIN_ACTIVATION_SPEED + 0.01
|
||||
self.set_lat_accels(0.5, 3.0, speed)
|
||||
self.scc_v.update(self.sm, True, False, speed, 0.0, 30.0)
|
||||
self.scc_v.update(self.sm, True, False, speed, 0.0, 30.0)
|
||||
|
||||
assert self.scc_v.state == VisionState.entering
|
||||
assert self.scc_v.is_active
|
||||
|
||||
def test_nonurgent_activation_has_no_target_cliff(self):
|
||||
v_ego = _MIN_ACTIVATION_SPEED + 0.01
|
||||
model_speed = 8.0
|
||||
self.update_lat_accels(0.5, 2.0, v_ego=v_ego, model_speed=model_speed)
|
||||
self.update_lat_accels(0.5, 2.0, v_ego=v_ego, model_speed=model_speed)
|
||||
|
||||
self.assert_approx(self.scc_v.v_target, 8.0)
|
||||
self.assert_approx(self.scc_v.output_v_target, v_ego)
|
||||
|
||||
def test_nonurgent_tightening_is_confirmed_and_rate_limited(self):
|
||||
self.enter_curve()
|
||||
initial_v_target = self.scc_v.output_v_target
|
||||
|
||||
for _ in range(_TARGET_TIGHTEN_CONFIRMATION_FRAMES - 1):
|
||||
self.update_lat_accels(0.5, 2.8)
|
||||
self.assert_approx(self.scc_v.output_v_target, initial_v_target)
|
||||
|
||||
self.update_lat_accels(0.5, 2.8)
|
||||
drop = initial_v_target - self.scc_v.output_v_target
|
||||
assert 0.0 < drop <= _TARGET_TIGHTEN_RATE * DT_MDL + 1e-9
|
||||
|
||||
def test_one_frame_curve_prediction_does_not_pulse_target(self):
|
||||
self.enter_curve()
|
||||
for _ in range(10):
|
||||
self.update_lat_accels(0.5, 2.2)
|
||||
stable_v_target = self.scc_v.output_v_target
|
||||
|
||||
self.update_lat_accels(0.5, 2.8)
|
||||
self.assert_approx(self.scc_v.output_v_target, stable_v_target)
|
||||
self.update_lat_accels(0.5, 2.2)
|
||||
|
||||
self.assert_approx(self.scc_v.output_v_target, stable_v_target)
|
||||
|
||||
def test_one_frame_release_does_not_reverse_target(self):
|
||||
self.enter_curve(_URGENT_PRED_LAT_ACC_TH)
|
||||
stable_v_target = self.scc_v.output_v_target
|
||||
|
||||
self.update_lat_accels(0.5, 2.2)
|
||||
self.assert_approx(self.scc_v.output_v_target, stable_v_target)
|
||||
self.update_lat_accels(0.5, _URGENT_PRED_LAT_ACC_TH)
|
||||
|
||||
self.assert_approx(self.scc_v.output_v_target, stable_v_target)
|
||||
|
||||
def test_urgent_predicted_curve_is_not_delayed(self):
|
||||
self.enter_curve()
|
||||
self.update_lat_accels(0.5, _URGENT_PRED_LAT_ACC_TH)
|
||||
|
||||
self.assert_approx(self.scc_v.output_v_target, self.scc_v._v_demand())
|
||||
|
||||
def test_current_curve_is_not_delayed(self):
|
||||
self.enter_curve()
|
||||
self.update_lat_accels(_TURNING_LAT_ACC_TH, 2.8)
|
||||
|
||||
self.assert_approx(self.scc_v.output_v_target, self.scc_v._v_demand())
|
||||
|
||||
def test_sequential_curve_confirms_release_and_tightens_urgently(self):
|
||||
self.enter_curve(3.0)
|
||||
for _ in range(20):
|
||||
self.update_lat_accels(0.5, 3.0)
|
||||
restrictive_v_target = self.scc_v.output_v_target
|
||||
|
||||
self.update_lat_accels(0.5, 1.4, a_ego=0.4)
|
||||
assert self.scc_v.state == VisionState.entering
|
||||
self.assert_approx(self.scc_v.output_v_target, restrictive_v_target)
|
||||
assert self.scc_v.output_a_target == 0.4
|
||||
|
||||
for _ in range(_TARGET_RELEASE_CONFIRMATION_FRAMES - 2):
|
||||
self.update_lat_accels(0.5, 1.4)
|
||||
self.assert_approx(self.scc_v.output_v_target, restrictive_v_target)
|
||||
|
||||
self.update_lat_accels(0.5, 1.4)
|
||||
released_v_target = self.scc_v.output_v_target
|
||||
assert 0.0 < released_v_target - restrictive_v_target <= _BELOW_EGO_TARGET_RELEASE_RATE * DT_MDL + 1e-9
|
||||
|
||||
self.update_lat_accels(0.5, 3.0, a_ego=-0.6)
|
||||
assert self.scc_v.state == VisionState.entering
|
||||
self.assert_approx(self.scc_v.output_v_target, restrictive_v_target)
|
||||
assert self.scc_v.output_a_target == -0.6
|
||||
|
||||
for _ in range(4):
|
||||
self.update_lat_accels(0.5, 1.4)
|
||||
self.assert_approx(self.scc_v.output_v_target, restrictive_v_target)
|
||||
self.update_lat_accels(0.5, 3.0)
|
||||
self.assert_approx(self.scc_v.output_v_target, restrictive_v_target)
|
||||
|
||||
def test_acceleration_is_continuous_through_planner_arbitration(self):
|
||||
car_control = messaging.new_message('carControl')
|
||||
car_control.carControl.enabled = True
|
||||
car_control.carControl.cruiseControl.override = False
|
||||
self.sm['carControl'] = car_control.carControl
|
||||
self.sm['carState'].vCruiseCluster = 108.0
|
||||
|
||||
planner: Any = LongitudinalPlannerSP.__new__(LongitudinalPlannerSP)
|
||||
planner.scc = SimpleNamespace(
|
||||
vision=self.scc_v,
|
||||
map=SimpleNamespace(output_v_target=V_CRUISE_UNSET, output_a_target=0.0),
|
||||
update=lambda sm, enabled, override, v_ego, a_ego, v_cruise: self.scc_v.update(sm, enabled, override, v_ego, a_ego, v_cruise),
|
||||
)
|
||||
planner.resolver = SimpleNamespace(
|
||||
speed_limit_valid=False,
|
||||
speed_limit_last_valid=False,
|
||||
speed_limit=0.0,
|
||||
speed_limit_final_last=0.0,
|
||||
distance=0.0,
|
||||
update=lambda _v_ego, _sm: None,
|
||||
)
|
||||
planner.sla = SimpleNamespace(
|
||||
output_v_target=V_CRUISE_UNSET,
|
||||
output_a_target=0.0,
|
||||
update=lambda *_args: None,
|
||||
)
|
||||
planner.events_sp = SimpleNamespace()
|
||||
|
||||
self.set_lat_accels(0.5, 2.2)
|
||||
planner.update_targets(self.sm, 20.0, -0.8, 30.0)
|
||||
planner.update_targets(self.sm, 20.0, -0.8, 30.0)
|
||||
assert planner.source == LongitudinalPlanSource.sccVision
|
||||
assert planner.output_a_target == -0.8
|
||||
|
||||
for planner_accel in (-2.0, 0.5, -0.2):
|
||||
planner.update_targets(self.sm, 20.0, planner_accel, 30.0)
|
||||
assert planner.source == LongitudinalPlanSource.sccVision
|
||||
assert planner.output_a_target == planner_accel
|
||||
|
||||
self.set_lat_accels(0.8, 0.8)
|
||||
for _ in range(int(30.0 / (_TARGET_RELEASE_RATE * DT_MDL)) + 10):
|
||||
planner.update_targets(self.sm, 20.0, 0.4, 30.0)
|
||||
assert planner.output_a_target == 0.4
|
||||
if planner.source == LongitudinalPlanSource.cruise:
|
||||
break
|
||||
else:
|
||||
self.fail("SCC Vision did not release to cruise")
|
||||
|
||||
planner.update_targets(self.sm, 20.0, 0.4, 30.0)
|
||||
assert self.scc_v.state == VisionState.enabled
|
||||
assert planner.source == LongitudinalPlanSource.cruise
|
||||
|
||||
@parameterized.expand(
|
||||
[
|
||||
("p97_just_above_threshold", True),
|
||||
("single_spike_filtered", False),
|
||||
("persistent_high_values", True),
|
||||
], names=["case", "should_enter"])
|
||||
],
|
||||
names=["case", "should_enter"],
|
||||
)
|
||||
def test_max_pred_lat_acc_uses_p97_and_threshold(self, case, should_enter):
|
||||
n = len(ModelConstants.T_IDXS)
|
||||
th = float(_ENTERING_PRED_LAT_ACC_TH)
|
||||
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
import gc
|
||||
from contextlib import ExitStack
|
||||
from unittest import mock
|
||||
|
||||
import numpy as np
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.selfdrive.test.longitudinal_maneuvers.plant import Plant
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanSource
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.vision_controller import _A_LAT_REG_MAX
|
||||
|
||||
|
||||
def _run_constant_curve(*, scc_enabled: bool, cruise: float, duration: float = 70.0) -> dict[str, np.ndarray]:
|
||||
gc.collect()
|
||||
curvature = 0.005
|
||||
plant = Plant(lead_relevancy=False, speed=30.0)
|
||||
planner = plant.planner
|
||||
planner.dec._enabled = False
|
||||
planner.scc.map.enabled = False
|
||||
planner.scc.vision.enabled = scc_enabled
|
||||
solver_failures = 0
|
||||
|
||||
with ExitStack() as patches:
|
||||
patches.enter_context(mock.patch.object(planner.dec, "_read_params", return_value=None))
|
||||
patches.enter_context(mock.patch.object(planner.scc.map, "update_params", return_value=None))
|
||||
patches.enter_context(mock.patch.object(planner.scc.vision, "_update_params", return_value=None))
|
||||
|
||||
original_mpc_reset = planner.mpc.reset
|
||||
|
||||
def record_mpc_reset(*args, **kwargs):
|
||||
nonlocal solver_failures
|
||||
solver_failures += int(planner.mpc.solution_status != 0)
|
||||
return original_mpc_reset(*args, **kwargs)
|
||||
|
||||
patches.enter_context(mock.patch.object(planner.mpc, "reset", side_effect=record_mpc_reset))
|
||||
|
||||
if scc_enabled:
|
||||
original_update_calculations = planner.scc.vision._update_calculations
|
||||
|
||||
def inject_constant_curvature(sm):
|
||||
velocities = np.asarray(sm['modelV2'].velocity.x, dtype=float)
|
||||
sm['modelV2'].orientationRate.z = (curvature * velocities).tolist()
|
||||
sm['controlsState'].curvature = curvature
|
||||
original_update_calculations(sm)
|
||||
|
||||
patches.enter_context(mock.patch.object(planner.scc.vision, "_update_calculations", side_effect=inject_constant_curvature))
|
||||
|
||||
original_update = planner.update
|
||||
|
||||
def enable_longitudinal(sm):
|
||||
sm['carControl'].enabled = True
|
||||
sm['carControl'].longActive = True
|
||||
original_update(sm)
|
||||
|
||||
patches.enter_context(mock.patch.object(planner, "update", side_effect=enable_longitudinal))
|
||||
rows = []
|
||||
while plant.current_time < duration:
|
||||
output = plant.step(v_cruise=cruise)
|
||||
rows.append(
|
||||
(
|
||||
plant.current_time,
|
||||
output['speed'],
|
||||
output['should_stop'],
|
||||
planner.scc.vision.is_active,
|
||||
planner.source == LongitudinalPlanSource.sccVision,
|
||||
planner.scc.vision.output_v_target,
|
||||
)
|
||||
)
|
||||
|
||||
data = np.asarray(rows, dtype=float)
|
||||
gc.collect()
|
||||
return {
|
||||
'time': data[:, 0],
|
||||
'speed': data[:, 1],
|
||||
'should_stop': data[:, 2],
|
||||
'active': data[:, 3],
|
||||
'scc_source': data[:, 4],
|
||||
'target': data[:, 5],
|
||||
'solver_failures': np.asarray(solver_failures),
|
||||
}
|
||||
|
||||
|
||||
class TestVisionControllerClosedLoop(OpenpilotTestCase):
|
||||
def test_constant_curve_recovers_like_stock_speed_cap(self):
|
||||
target = (_A_LAT_REG_MAX / 0.005) ** 0.5
|
||||
scc = _run_constant_curve(scc_enabled=True, cruise=30.0)
|
||||
stock = _run_constant_curve(scc_enabled=False, cruise=target)
|
||||
scc_final = scc['speed'][scc['time'] >= 60.0]
|
||||
stock_final = stock['speed'][stock['time'] >= 60.0]
|
||||
|
||||
# The generated solver can report platform-specific failures for the
|
||||
# synthetic no-lead plant. The feature must not make that stock baseline
|
||||
# worse; requiring an absolute zero would hide a harness difference as a
|
||||
# controller regression.
|
||||
assert scc['solver_failures'] <= stock['solver_failures']
|
||||
assert not scc['should_stop'].any()
|
||||
assert np.all(scc['active'][scc['time'] >= 60.0])
|
||||
assert np.all(scc['scc_source'][scc['time'] >= 60.0])
|
||||
assert np.allclose(scc['target'][scc['time'] >= 60.0], target)
|
||||
assert scc_final.min() >= target - 1.0
|
||||
assert abs(scc_final.mean() - stock_final.mean()) < 0.5
|
||||
assert abs(scc_final.min() - stock_final.min()) < 1.0
|
||||
assert abs(scc_final.max() - stock_final.max()) < 1.0
|
||||
+89
-61
@@ -23,25 +23,21 @@ _ENTERING_PRED_LAT_ACC_TH = 1.3 # Predicted Lat Acc threshold to trigger enteri
|
||||
_ABORT_ENTERING_PRED_LAT_ACC_TH = 1.1 # Predicted Lat Acc threshold to abort entering state if speed drops.
|
||||
|
||||
_TURNING_LAT_ACC_TH = 1.6 # Lat Acc threshold to trigger turning state.
|
||||
_URGENT_PRED_LAT_ACC_TH = 3. # Predicted Lat Acc threshold that requires an immediate speed reduction.
|
||||
|
||||
_LEAVING_LAT_ACC_TH = 1.3 # Lat Acc threshold to trigger leaving turn state.
|
||||
_FINISH_LAT_ACC_TH = 1.1 # Lat Acc threshold to trigger the end of the turn cycle.
|
||||
|
||||
_A_LAT_REG_MAX = 2. # Maximum lateral acceleration
|
||||
|
||||
_NO_OVERSHOOT_TIME_HORIZON = 4. # s. Time to use for velocity desired based on a_target when not overshooting.
|
||||
|
||||
# Lookup table for the minimum smooth deceleration during the ENTERING state
|
||||
# depending on the actual maximum absolute lateral acceleration predicted on the turn ahead.
|
||||
_ENTERING_SMOOTH_DECEL_V = [-0.2, -1.] # min decel value allowed on ENTERING state
|
||||
_ENTERING_SMOOTH_DECEL_BP = [1.3, 3.] # absolute value of lat acc ahead
|
||||
|
||||
# Lookup table for the acceleration for the TURNING state
|
||||
# depending on the current lateral acceleration of the vehicle.
|
||||
_TURNING_ACC_V = [0.5, 0., -0.4] # acc value
|
||||
_TURNING_ACC_BP = [1.5, 2.3, 3.] # absolute value of current lat acc
|
||||
|
||||
_LEAVING_ACC = 0.5 # Conformable acceleration to regain speed while leaving a turn.
|
||||
_RELIEF_CONFIRMATION_FRAMES = max(1, int(round(0.5 / DT_MDL)))
|
||||
_TARGET_TIGHTEN_CONFIRMATION_FRAMES = max(1, int(round(0.1 / DT_MDL)))
|
||||
_TARGET_RELEASE_CONFIRMATION_FRAMES = max(1, int(round(0.15 / DT_MDL)))
|
||||
_TARGET_TIGHTEN_RATE = 5. # m/s^2
|
||||
_TARGET_RELEASE_RATE = 1. # m/s^2
|
||||
_BELOW_EGO_TARGET_RELEASE_RATE = 3. # m/s^2
|
||||
_MIN_PRED_SPEED = 1. # m/s
|
||||
_MIN_ACTIVATION_SPEED = 10. # m/s
|
||||
|
||||
|
||||
class SmartCruiseControlVision:
|
||||
@@ -65,14 +61,62 @@ class SmartCruiseControlVision:
|
||||
self.state = VisionState.disabled
|
||||
self.current_lat_acc = 0.
|
||||
self.max_pred_lat_acc = 0.
|
||||
self.relief_frames = 0
|
||||
self.tighten_frames = 0
|
||||
self.release_frames = 0
|
||||
|
||||
def _v_demand(self) -> float:
|
||||
return max(MIN_V, min(self.v_target, self.v_cruise_setpoint))
|
||||
|
||||
def _curve_is_urgent(self) -> bool:
|
||||
return self.current_lat_acc >= _TURNING_LAT_ACC_TH or self.max_pred_lat_acc >= _URGENT_PRED_LAT_ACC_TH
|
||||
|
||||
def _filtered_v_target(self) -> float:
|
||||
demand = self._v_demand()
|
||||
|
||||
if self.output_v_target == V_CRUISE_UNSET:
|
||||
self.tighten_frames = 0
|
||||
self.release_frames = 0
|
||||
if self._curve_is_urgent():
|
||||
return demand
|
||||
return max(demand, min(self.v_ego, self.v_cruise_setpoint))
|
||||
|
||||
if demand < self.output_v_target:
|
||||
self.release_frames = 0
|
||||
if self._curve_is_urgent():
|
||||
self.tighten_frames = 0
|
||||
return demand
|
||||
|
||||
self.tighten_frames += 1
|
||||
if self.tighten_frames < _TARGET_TIGHTEN_CONFIRMATION_FRAMES:
|
||||
return self.output_v_target
|
||||
return max(demand, self.output_v_target - _TARGET_TIGHTEN_RATE * DT_MDL)
|
||||
|
||||
self.tighten_frames = 0
|
||||
releasing_brake = self.output_v_target < min(self.v_ego, demand)
|
||||
if not releasing_brake and self.relief_frames < _RELIEF_CONFIRMATION_FRAMES:
|
||||
self.release_frames = 0
|
||||
return self.output_v_target
|
||||
|
||||
if demand > self.output_v_target:
|
||||
self.release_frames += 1
|
||||
if self.release_frames < _TARGET_RELEASE_CONFIRMATION_FRAMES:
|
||||
return self.output_v_target
|
||||
else:
|
||||
self.release_frames = 0
|
||||
|
||||
release_rate = _BELOW_EGO_TARGET_RELEASE_RATE if releasing_brake else _TARGET_RELEASE_RATE
|
||||
return min(demand, self.output_v_target + release_rate * DT_MDL)
|
||||
|
||||
def get_a_target_from_control(self) -> float:
|
||||
return self.a_target
|
||||
return self.a_ego
|
||||
|
||||
def get_v_target_from_control(self) -> float:
|
||||
if self.is_active:
|
||||
return max(self.v_target, MIN_V) + self.a_target * _NO_OVERSHOOT_TIME_HORIZON
|
||||
return self._filtered_v_target()
|
||||
|
||||
self.tighten_frames = 0
|
||||
self.release_frames = 0
|
||||
return V_CRUISE_UNSET
|
||||
|
||||
def _update_params(self) -> None:
|
||||
@@ -82,25 +126,27 @@ class SmartCruiseControlVision:
|
||||
def _update_calculations(self, sm: messaging.SubMaster) -> None:
|
||||
if not self.long_enabled:
|
||||
return
|
||||
else:
|
||||
rate_plan = np.array(np.abs(sm['modelV2'].orientationRate.z))
|
||||
vel_plan = np.array(sm['modelV2'].velocity.x)
|
||||
|
||||
self.current_lat_acc = self.v_ego ** 2 * abs(sm['controlsState'].curvature)
|
||||
rate_plan = np.asarray(np.abs(sm['modelV2'].orientationRate.z), dtype=float)
|
||||
vel_plan = np.asarray(sm['modelV2'].velocity.x, dtype=float)
|
||||
size = min(len(rate_plan), len(vel_plan))
|
||||
rate_plan, vel_plan = rate_plan[:size], vel_plan[:size]
|
||||
valid = np.isfinite(rate_plan) & np.isfinite(vel_plan) & (vel_plan >= _MIN_PRED_SPEED)
|
||||
|
||||
# get the maximum lat accel from the model
|
||||
predicted_lat_accels = rate_plan * vel_plan
|
||||
self.max_pred_lat_acc = np.percentile(predicted_lat_accels, 97)
|
||||
|
||||
# get the maximum curve based on the current velocity
|
||||
v_ego = max(self.v_ego, 0.1) # ensure a value greater than 0 for calculations
|
||||
max_curve = self.max_pred_lat_acc / (v_ego**2)
|
||||
|
||||
# Get the target velocity for the maximum curve
|
||||
self.v_target = (_A_LAT_REG_MAX / max_curve) ** 0.5
|
||||
self.current_lat_acc = self.v_ego ** 2 * abs(sm['controlsState'].curvature)
|
||||
self.max_pred_lat_acc = 0.
|
||||
self.v_target = V_CRUISE_UNSET
|
||||
if np.any(valid):
|
||||
self.max_pred_lat_acc = float(np.percentile(rate_plan[valid] * vel_plan[valid], 97))
|
||||
max_pred_curvature = float(np.percentile(rate_plan[valid] / vel_plan[valid], 97))
|
||||
if max_pred_curvature > 0.:
|
||||
self.v_target = min(float((_A_LAT_REG_MAX / max_pred_curvature) ** 0.5), V_CRUISE_UNSET)
|
||||
|
||||
def _update_state_machine(self) -> tuple[bool, bool]:
|
||||
# ENABLED, ENTERING, TURNING, LEAVING, OVERRIDING
|
||||
relief = self.current_lat_acc < _FINISH_LAT_ACC_TH and self.max_pred_lat_acc < _ABORT_ENTERING_PRED_LAT_ACC_TH
|
||||
self.relief_frames = self.relief_frames + 1 if self.state in ACTIVE_STATES and relief else 0
|
||||
|
||||
if self.state != VisionState.disabled:
|
||||
# longitudinal and feature disable always have priority in a non-disabled state
|
||||
if not self.long_enabled or not self.enabled:
|
||||
@@ -112,7 +158,7 @@ class SmartCruiseControlVision:
|
||||
# ENABLED
|
||||
if self.state == VisionState.enabled:
|
||||
# Do not enter a turn control cycle if the speed is low.
|
||||
if self.v_ego <= MIN_V:
|
||||
if self.v_ego <= _MIN_ACTIVATION_SPEED:
|
||||
pass
|
||||
# If significant lateral acceleration is predicted ahead, then move to Entering turn state.
|
||||
elif self.max_pred_lat_acc >= _ENTERING_PRED_LAT_ACC_TH:
|
||||
@@ -128,23 +174,26 @@ class SmartCruiseControlVision:
|
||||
# Transition to Turning if current lateral acceleration is over the threshold.
|
||||
if self.current_lat_acc >= _TURNING_LAT_ACC_TH:
|
||||
self.state = VisionState.turning
|
||||
# Abort if the predicted lateral acceleration drops
|
||||
elif self.max_pred_lat_acc < _ABORT_ENTERING_PRED_LAT_ACC_TH:
|
||||
self.state = VisionState.enabled
|
||||
# Begin releasing only after both current and predicted lateral acceleration stay clear.
|
||||
elif self.relief_frames >= _RELIEF_CONFIRMATION_FRAMES:
|
||||
self.state = VisionState.leaving
|
||||
|
||||
# TURNING
|
||||
elif self.state == VisionState.turning:
|
||||
# Transition to Leaving if current lateral acceleration drops below a threshold.
|
||||
# Transition out of Turning if current lateral acceleration drops below a threshold.
|
||||
if self.current_lat_acc <= _LEAVING_LAT_ACC_TH:
|
||||
self.state = VisionState.leaving
|
||||
self.state = VisionState.entering if self.max_pred_lat_acc >= _ENTERING_PRED_LAT_ACC_TH else VisionState.leaving
|
||||
|
||||
# LEAVING
|
||||
elif self.state == VisionState.leaving:
|
||||
# Transition back to Turning if current lateral acceleration goes back over the threshold.
|
||||
if self.current_lat_acc >= _TURNING_LAT_ACC_TH:
|
||||
self.state = VisionState.turning
|
||||
# Finish if current lateral acceleration goes below a threshold.
|
||||
elif self.current_lat_acc < _FINISH_LAT_ACC_TH:
|
||||
# Start a new turn cycle immediately if another curve is predicted.
|
||||
elif self.max_pred_lat_acc >= _ENTERING_PRED_LAT_ACC_TH:
|
||||
self.state = VisionState.entering
|
||||
# Finish after confirmed relief and a gradual release to the cruise setpoint.
|
||||
elif self.relief_frames >= _RELIEF_CONFIRMATION_FRAMES and self.output_v_target >= self.v_cruise_setpoint:
|
||||
self.state = VisionState.enabled
|
||||
|
||||
# DISABLED
|
||||
@@ -157,32 +206,11 @@ class SmartCruiseControlVision:
|
||||
|
||||
enabled = self.state in ENABLED_STATES
|
||||
active = self.state in ACTIVE_STATES
|
||||
if not active:
|
||||
self.relief_frames = 0
|
||||
|
||||
return enabled, active
|
||||
|
||||
def _update_solution(self) -> float:
|
||||
# DISABLED, ENABLED, OVERRIDING
|
||||
if self.state not in ACTIVE_STATES:
|
||||
# when not overshooting, calculate v_turn as the speed at the prediction horizon when following
|
||||
# the smooth deceleration.
|
||||
a_target = self.a_ego
|
||||
# ENTERING
|
||||
elif self.state == VisionState.entering:
|
||||
# when not overshooting, target a smooth deceleration in preparation for a sharp turn to come.
|
||||
a_target = np.interp(self.max_pred_lat_acc, _ENTERING_SMOOTH_DECEL_BP, _ENTERING_SMOOTH_DECEL_V)
|
||||
# TURNING
|
||||
elif self.state == VisionState.turning:
|
||||
# When turning, we provide a target acceleration that is comfortable for the lateral acceleration felt.
|
||||
a_target = np.interp(self.current_lat_acc, _TURNING_ACC_BP, _TURNING_ACC_V)
|
||||
# LEAVING
|
||||
elif self.state == VisionState.leaving:
|
||||
# When leaving, we provide a comfortable acceleration to regain speed.
|
||||
a_target = _LEAVING_ACC
|
||||
else:
|
||||
raise NotImplementedError(f"SCC-V state not supported: {self.state}")
|
||||
|
||||
return a_target
|
||||
|
||||
def update(self, sm: messaging.SubMaster, long_enabled: bool, long_override: bool, v_ego: float, a_ego: float,
|
||||
v_cruise_setpoint: float) -> None:
|
||||
self.long_enabled = long_enabled
|
||||
@@ -195,7 +223,7 @@ class SmartCruiseControlVision:
|
||||
self._update_calculations(sm)
|
||||
|
||||
self.is_enabled, self.is_active = self._update_state_machine()
|
||||
self.a_target = self._update_solution()
|
||||
self.a_target = self.a_ego
|
||||
|
||||
self.output_v_target = self.get_v_target_from_control()
|
||||
self.output_a_target = self.get_a_target_from_control()
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
|
||||
from openpilot.common.realtime import DT_CTRL
|
||||
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState
|
||||
|
||||
|
||||
# Confirmed stop. Owner 2026-10-08: "can the brake be added 0.2 s
|
||||
# after the stop, but only when it is 100% certain the car has stopped?". The wheel-pulse standstill flag alone is not
|
||||
# certain: on 2 of 30 engaged stops (route 00000100 18:59:09, 19:37:50) the pulse counter still stepped 0.06 s after
|
||||
# it (~0.2 m). The body's own signature is: while the car still rolls it decelerates; at the real stop the deceleration
|
||||
# collapses (the body pitches back). Using BOTH - the flag AND the longitudinal acceleration (calibrated pose) risen
|
||||
# STOP_CONFIRM_RISE above the braking level it had above STOP_LEVEL_V - no wheel pulse followed in any of the 30 stops
|
||||
# (routes fe..118). The hold then starts STOP_CONFIRM_DELAY later instead of after the 1.0 s hold delay; without a
|
||||
# confirmation (a very light stop, no pitch-back) the 1.0 s delay applies as before.
|
||||
STOP_LEVEL_V = 0.4 # m/s (~1.4 km/h): above this the braking level is tracked
|
||||
STOP_LEVEL_TAU = 0.2 # s
|
||||
STOP_CONFIRM_RISE = 0.2 # m/s^2
|
||||
STOP_CONFIRM_DELAY = 0.2 # s
|
||||
|
||||
|
||||
class StopConfirm:
|
||||
def __init__(self):
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
self.level: float | None = None
|
||||
self.collapsed = False
|
||||
self.confirmed_t = -1.0
|
||||
|
||||
def update(self, standstill: bool, v_ego: float, a_long: float | None) -> bool:
|
||||
"""returns True once the stop is confirmed and STOP_CONFIRM_DELAY has passed"""
|
||||
if a_long is None or not math.isfinite(a_long):
|
||||
self.reset()
|
||||
return False
|
||||
if v_ego > STOP_LEVEL_V and not standstill:
|
||||
alpha = DT_CTRL / (STOP_LEVEL_TAU + DT_CTRL)
|
||||
self.level = a_long if self.level is None else self.level + alpha * (a_long - self.level)
|
||||
self.collapsed = False
|
||||
self.confirmed_t = -1.0
|
||||
return False
|
||||
if self.level is not None and a_long >= self.level + STOP_CONFIRM_RISE:
|
||||
self.collapsed = True
|
||||
if standstill and self.collapsed:
|
||||
self.confirmed_t = 0.0 if self.confirmed_t < 0.0 else self.confirmed_t + DT_CTRL
|
||||
elif not standstill:
|
||||
self.confirmed_t = -1.0
|
||||
return self.confirmed_t >= STOP_CONFIRM_DELAY - 1e-9
|
||||
|
||||
|
||||
class StoppingController:
|
||||
"""Optional terminal-stop policy applied after stock LongControl.update()."""
|
||||
|
||||
STOPPING_DECEL_RATE = 0.3 # m/s^2/s
|
||||
# once the wheels really stand (wheel pulse) and the hold delay has passed, press on firmly to stopAccel: the driver
|
||||
# presses to 1000-2400 N within 1-2 s of stopping (owner 2026-10-02: the car must not be able to move again, also on
|
||||
# a slope). 1.0 m/s^2/s reaches the default -2.0 (~2900 N) in ~1.9 s; the old 0.5 took ~3.7 s.
|
||||
STANDSTILL_HOLD_RATE = 1.0 # m/s^2/s
|
||||
# The standstill flag is the real wheel stop (wheel pulse counter). Driver 2026-10-05, after a slope stop at 15:53
|
||||
# where "at the last moment the hold locked early with a jerk": "let the car sit at -0.25 a while longer, then add the
|
||||
# parking force" -> 1.0 s at the end value before the ramp to stopAccel (was 0.2 s; the 10-03 "never more than 0.2 s"
|
||||
# was about the auto hold under a pedal, not this ramp). If the car creeps on a slope during that second the creep
|
||||
# path below raises the request on its own (CREEP_RATE_GROWTH), so the wait costs nothing on the flat and is bounded
|
||||
# on a hill.
|
||||
STANDSTILL_HOLD_DELAY_LEAD = 1.0 # s
|
||||
STANDSTILL_HOLD_DELAY_NO_LEAD = 1.0 # s
|
||||
STOPPING_FREEZE_MAX = 2.0 # s
|
||||
STOPPING_EXIT_DEBOUNCE = 0.2 # s
|
||||
# Engaging at a standstill (driver 2026-10-06, route 0000010a 23:58:49: "pulling away with no lead, just before the car
|
||||
# really moves there is a very short hard brake, then it goes"). The car stood in the auto hold (1360 N); engaging
|
||||
# dropped the hold to 0 N, the plan's first frame still said stop, so the stopping state started the standstill hold
|
||||
# ramp at once (it had stood long past the hold delay) - -0.21 m/s^2 within 0.2 s, which the PCM turned into a
|
||||
# 1600 N brake pulse (IMU +0.6 m/s^2 jolt) before the launch 0.4 s later. For ENGAGE_STANDSTILL_GRACE after engaging
|
||||
# at a standstill with no lead the request stays at zero instead: the launch follows straight away; if the plan
|
||||
# really wants to stay stopped, the hold ramp starts after the grace.
|
||||
ENGAGE_STANDSTILL_GRACE = 0.6 # s
|
||||
STOPPING_FOLLOW_MIN = -0.10 # m/s^2
|
||||
STOPPING_FOLLOW_RATE = 2.0 # m/s^3
|
||||
CREEP_V_MIN = 0.03 # m/s
|
||||
CREEP_RATE_GROWTH = 1.5 # m/s^2/s per second of creep
|
||||
CREEP_RATE_MAX = 1.0 # m/s^2/s
|
||||
|
||||
# The end of the stop. First road test of the template rule (route 000000ec, 2026-10-03, four op stops): holding
|
||||
# END_REQUEST -0.15 (~660 N) from 3 km/h was a STEP - the plan was at -0.63..-0.78 (1160-1560 N) when the car crossed
|
||||
# 3 km/h and the request fell to -0.15 within 0.3 s; the force dropped to 280-520 N, the car sped UP again (+0.1..+0.3
|
||||
# m/s^2, 1.7 -> 2.5 km/h) on its creep torque and then needed 3.4-4.3 s and 760-1000 N (-0.2..-0.25 m/s^2) to stop.
|
||||
# The driver felt exactly that: "at 3 km/h the brake suddenly lightens - wrong - there must be a smooth hand-over".
|
||||
# The driver's own hard stop that night (23:45:04, 36 km/h, 4880 N peak) still ended smoothly because the pedal came
|
||||
# off CONTINUOUSLY: -aEgo ~ 1.6 + 0.25 v (r 0.86), -3.4 m/s^2 at 27 km/h, -2.6 at 10, -1.4 at 3, -1.0 at 1 km/h -
|
||||
# the decel falls roughly linearly with speed to the wheel stop, never a step, and does not go light at the end.
|
||||
# So below BLEND_V the request follows a line in speed from the request it had at BLEND_V (a_entry) down to
|
||||
# END_REQUEST at 0 km/h: blend(v) = END_REQUEST + (a_entry - END_REQUEST) * (v / BLEND_V) ** BLEND_POW. It is a FLOOR
|
||||
# on the plan: the firmer of the plan and the line is sent, so the planner's own curve still wins while it is braking
|
||||
# harder (a late-seen lead), and the line takes over only where the planner eases off. A plan that wants to go
|
||||
# (a_target > 0, the lead moved off) releases it; after standstill the hold ramp to stopAccel applies unchanged.
|
||||
# END_REQUEST -0.50 (~1080 N by BF ~ 480 + 1200 * |req|) is the lightest request that still decelerates under ACC:
|
||||
# on this route 760-1000 N gave only -0.2..-0.25 m/s^2 (the pedal is not pressed, so the hybrid keeps its creep
|
||||
# torque), while -0.63..-0.78 gave -0.6..-1.07 at 3 km/h. The driver's template ends at 0.5-0.7 m/s^2.
|
||||
# Driver 2026-10-03 afternoon, after the -0.50 / -0.40 / -0.35 flat ends: "from 5 km/h to 0 use a formula: -0.5 at
|
||||
# 5 km/h, -0.3 at 0 km/h, interpolated - firmer at 5 km/h, then easing as the car comes to rest". So the line is
|
||||
# now piecewise linear through (BLEND_V, a_entry) -> (END_V_HI, END_HI) -> (0, END_LO); a_entry is at least END_HI.
|
||||
BLEND_V = 10.0 / 3.6 # m/s: the line starts here, at the request the car had at that moment
|
||||
END_V_HI = 5.0 / 3.6 # m/s
|
||||
END_HI = -0.65 # m/s^2 at END_V_HI (~1200 N); -0.60 -> -0.65 2026-10-08, see END_CURVE_A
|
||||
END_LO = -0.30 # m/s^2 at 0-1 km/h; driver 2026-10-04: -0.30 "closer to perfect", -0.25 the threshold; 2026-10-06 back to -0.30
|
||||
# Driver 2026-10-04 (route 000000f2): "between 5 and 0 km/h not linear but parabolic: steeper from 5 to 2, flat from
|
||||
# 2 to 0" -> x^2 parabola -0.50/-0.41/-0.34/-0.29/-0.26/-0.25 ("closer to perfect"). An S-shaped table
|
||||
# (-0.50/-0.47/-0.40/-0.29/-0.25/-0.24) was "not better" and reverted. Then the driver gave his own points, close to
|
||||
# the parabola but a touch lighter in the middle: 4 km/h -0.40, 3 -0.33, 2 -0.27, 1 -0.25, 0 -0.25. Held as a table,
|
||||
# linearly interpolated at the actual speed every 10 ms (continuous curve).
|
||||
END_CURVE_V = [0.0, 1.0 / 3.6, 2.0 / 3.6, 3.0 / 3.6, 4.0 / 3.6, 5.0 / 3.6] # m/s
|
||||
# Driver 2026-10-06 (route 00000100, seven manual stops 19:45-19:59, averages 5->2 km/h 0.83, 2->0.3 km/h 0.45 m/s^2;
|
||||
# the lightest steady one, 19:56:19, 0.56 / 0.37): "the manual stops are still a touch heavy; give 5-2 km/h a little
|
||||
# more, but already be light at 1 km/h, or reaching -0.25 only at 0 km/h may still nod; keep 1-0 as it was". So a
|
||||
# parabola with its flat top at 5 km/h (END_HI) that bends down to END_LO by 1 km/h, then flat END_LO to the stop:
|
||||
# a = END_LO + (END_HI - END_LO) * (1 - ((5 - v) / 4)^2) -> -0.60/-0.58/-0.51/-0.40/-0.25/-0.25 at 5..0 km/h.
|
||||
# Driver 2026-10-06 night: "the end of the stop is not ideal, make 1-0 -0.3": END_LO -0.30, same parabola above it
|
||||
# -> -0.60/-0.58/-0.53/-0.43/-0.30/-0.30.
|
||||
# Driver 2026-10-08 (routes 00000112-00000116): "just before the stop, around 4-2 km/h, the brake feels released a little
|
||||
# too much". Since the low-speed PID hold (c14c0dbae1) the integrator no longer adds its catch-up there, and the car
|
||||
# delivered -0.44 / -0.55 / -0.49 / -0.45 at 5-4 / 4-3 / 3-2 / 2-1 km/h (BRAKE 0xA6 870-1010 N) against ~-0.7 / -0.7 /
|
||||
# -0.6 before. Driver chose the table 5 km/h -0.65, 4 -0.65, 3 -0.60, 2 -0.48, 1-0 -0.30 unchanged (expected delivered
|
||||
# ~-0.61 / -0.55 / -0.48 at 4-3 / 3-2 / 2-1 km/h). Was -0.60/-0.58/-0.53/-0.43/-0.30/-0.30.
|
||||
END_CURVE_A = [END_LO, END_LO, -0.48, -0.60, -0.65, END_HI] # m/s^2
|
||||
END_PLAN_MIN = -0.25 # m/s^2: the plan must be braking this much at BLEND_V to count as stopping (a crawl-follow hovers near 0)
|
||||
END_RATE = 2.0 # m/s^3: how fast the request may move toward the line
|
||||
|
||||
# Leaving the standstill hold (driver 2026-10-04, route 000000fa 14:43:32: "the brake release at launch is a jerk, as
|
||||
# if let go suddenly; make it linear and slow"). There the request went from the -2.0 hold (3960 N) to +0.7 in 0.5 s
|
||||
# at the Toyota windup limit: the PCM bled 3960 -> 0 N in 0.6 s while the gas request was already +0.6, and the car
|
||||
# lurched at +1.7 m/s^2 for the first 0.1 s. The brake pressure cannot move the car while the request is well below
|
||||
# zero, so the release keeps the fast rate down there and goes LINEAR AND SLOW only through the hand-over band where
|
||||
# the clamp falls off and the creep torque takes over: RELEASE_FAST up to RELEASE_BAND_LO, RELEASE_SLOW from there
|
||||
# up to RELEASE_BAND_HI, then the plan again. The first motion comes ~0.1 s later than before, the lurch is gone.
|
||||
RELEASE_FAST = 4.0 # m/s^3 (the stock windup limit)
|
||||
RELEASE_SLOW = 1.5 # m/s^3
|
||||
RELEASE_BAND_LO = -0.6 # m/s^2
|
||||
RELEASE_BAND_HI = 0.4 # m/s^2
|
||||
|
||||
# The tnpb2 additions below run on every car.
|
||||
# B - re-roll at a standstill. Route 0000010e 17:38:48: the car stood behind the lead at the
|
||||
# end request -0.30 (BRAKE 0xA6 640 N, less than the ~760-800 N the hybrid's creep torque needs); 0.9 s later, still
|
||||
# inside the 1.0 s hold delay, all four wheel speeds read 0.8 km/h for 1.1 s (the pulse counter did not step: < 0.2 m)
|
||||
# while the creep path raised the request only at 0.3 -> 1.0 m/s^2/s; it stood again once the force reached ~1300 N
|
||||
# (-0.8). Now, once the car moves again after it has stood (creeping), the request steps down to REROLL_ACCEL at
|
||||
# REROLL_JERK (0.05 s from -0.30), then continues on the creep / hold path. A car that stays still is not affected.
|
||||
REROLL_ACCEL = -0.6 # m/s^2 (~1100 N or more)
|
||||
REROLL_JERK = 6.0 # m/s^3
|
||||
# C - lighter standstill hold on the flat. The launch from a standstill takes 2.1 s
|
||||
# (median, routes fc..10e) from the lead moving to the wheels moving, against 1.7 s before 2026-10-04; most of the
|
||||
# wait is the PCM bleeding the -2.0 hold (3240 N, BRAKE 0xA6) - 1.1 s from 3240 N to 0 on route 0000010d 23:56:58.
|
||||
# On the flat the hold now stops at FLAT_HOLD_ACCEL (~2000 N, still 2.5x the creep torque and above the 1360 N the
|
||||
# auto brake hold uses on the flat); on a slope (|pitch - FLAT_PITCH_DEG| >= SLOPE_PITCH_DEG, as in auto_brake_hold)
|
||||
# or without a pitch, or while the car creeps, the stock stopAccel applies.
|
||||
FLAT_HOLD_ACCEL = -1.2 # m/s^2
|
||||
FLAT_PITCH_DEG = 1.0 # deg, what this car reports standing on the flat
|
||||
SLOPE_PITCH_DEG = 2.0 # deg away from flat that counts as a slope
|
||||
# H - creep-follow stop. Owner 2026-10-07: "at a standstill the lead moves a little, we
|
||||
# follow and stop again - still not soft; release smoothly, roll forward, then stop smoothly with -0.24". Route
|
||||
# 0000010d 23:56:59: after the release the plan turned to -0.4 within 0.2 s, but the command stayed positive for
|
||||
# ~0.7 s (soft-onset shaper + a PID integral left from the launch), the end-of-stop line asked -0.48 at 2.5 km/h, and
|
||||
# the brake bit 0 -> 1640 N in 0.4 s (aEgo +0.36 -> -0.88). Now, when the car has left a standstill less than
|
||||
# CF_WINDOW ago and has not gone faster than CF_V_MAX, a stop asked by the plan (a_target <= CF_TRIGGER) is made with
|
||||
# CF_END to the wheel stop: any positive request is dropped at once, the braking side is entered at CF_RATE (below the
|
||||
# onset shaper's threshold, so it passes unshaped), and the end-of-stop line is not used. The plan still wins when it
|
||||
# needs more: from CF_PLAN_SOFT the request blends to the plan, which it follows below CF_PLAN_FIRM (a lead that
|
||||
# brakes, or the gap closing because -0.24 is too little). A plan that wants to go (a_target > 0) ends it.
|
||||
CF_WINDOW = 10.0 # s
|
||||
CF_V_MAX = 8.0 / 3.6 # m/s
|
||||
CF_TRIGGER = -0.15 # m/s^2
|
||||
CF_END = -0.24 # m/s^2
|
||||
CF_PLAN_SOFT = -0.45 # m/s^2
|
||||
CF_PLAN_FIRM = -0.70 # m/s^2
|
||||
CF_RATE = 0.9 # m/s^3
|
||||
# ... and once the wheels stop after such a creep-follow stop (owner 2026-10-07, route 00000112 23:16:57: "stopped
|
||||
# with -0.24, then the hold came as a second brake"; there the request sat at -0.24 (~650 N, less than the creep
|
||||
# torque) for the 1.0 s hold delay, then ramped to -1.2 within ~1 s - two separate pushes. Owner: "if it stops with
|
||||
# -0.24, just add a little brake after the stop"): the brake is added straight away and smoothly, CF_SETTLE_ACCEL
|
||||
# within ~0.5 s (CF_SETTLE_JERK), enough to hold the creep torque, then the rest of the hold very slowly
|
||||
# (CF_HOLD_RATE) - one continuous motion, no hold delay. A re-roll still goes through B.
|
||||
CF_SETTLE_ACCEL = -0.6 # m/s^2 (~1100 N)
|
||||
CF_SETTLE_JERK = 0.72 # m/s^3: -0.24 -> -0.6 in 0.5 s
|
||||
CF_HOLD_RATE = 0.3 # m/s^2/s: -0.6 -> -1.2 in 2 s
|
||||
|
||||
def __init__(self, stop_accel):
|
||||
self.stop_accel = stop_accel
|
||||
self.stop_confirm = StopConfirm()
|
||||
self.cf_t: float | None = None # time since leaving a standstill (creep-follow window)
|
||||
self.cf_vmax = 0.0
|
||||
self.cf_stopping = False
|
||||
self.cf_stopped = False # the wheels stopped at the end of a creep-follow stop
|
||||
self.standstill_t = 0.0
|
||||
self.stopping_t = 0.0
|
||||
self.go_t = 0.0
|
||||
self.engaged_t = 0.0
|
||||
self.stopped_once = False
|
||||
self.creep_t = 0.0
|
||||
self.end_active = False
|
||||
self.a_entry = self.END_HI
|
||||
self.releasing = False
|
||||
|
||||
def _blend(self, v_ego):
|
||||
if v_ego < self.END_V_HI:
|
||||
return float(np.interp(max(v_ego, 0.0), self.END_CURVE_V, self.END_CURVE_A))
|
||||
return float(np.interp(v_ego, [self.END_V_HI, self.BLEND_V], [self.END_HI, self.a_entry]))
|
||||
|
||||
def _end_request(self, a_target, prev_accel, v_ego):
|
||||
target = min(a_target, self._blend(v_ego)) # the firmer of the plan and the line
|
||||
step = self.END_RATE * DT_CTRL
|
||||
return float(np.clip(target, prev_accel - step, prev_accel + step))
|
||||
|
||||
def _hold_floor(self, pitch, creeping):
|
||||
if creeping or pitch is None or not math.isfinite(pitch):
|
||||
return self.stop_accel
|
||||
if abs(math.degrees(pitch) - self.FLAT_PITCH_DEG) >= self.SLOPE_PITCH_DEG:
|
||||
return self.stop_accel
|
||||
return max(self.stop_accel, self.FLAT_HOLD_ACCEL)
|
||||
|
||||
def _cf_request(self, a_target, prev_accel):
|
||||
target = float(np.interp(a_target, [self.CF_PLAN_FIRM, self.CF_PLAN_SOFT], [self.CF_PLAN_FIRM, self.CF_END]))
|
||||
target = min(target, a_target) if a_target < self.CF_PLAN_FIRM else target
|
||||
if target < prev_accel:
|
||||
return max(target, min(prev_accel, 0.0) - self.CF_RATE * DT_CTRL)
|
||||
return min(target, prev_accel + self.END_RATE * DT_CTRL)
|
||||
|
||||
def _update_creep_follow(self, state, CS, a_target):
|
||||
if state == LongCtrlState.off:
|
||||
self.cf_t, self.cf_stopping, self.cf_stopped = None, False, False
|
||||
return False
|
||||
if CS.standstill:
|
||||
if self.cf_stopping:
|
||||
self.cf_stopped = True
|
||||
self.cf_t = 0.0 if self.standstill_t > 0.5 else self.cf_t
|
||||
self.cf_vmax, self.cf_stopping = 0.0, False
|
||||
return False
|
||||
self.cf_stopped = False
|
||||
if self.cf_t is None:
|
||||
return False
|
||||
self.cf_t += DT_CTRL
|
||||
self.cf_vmax = max(self.cf_vmax, CS.vEgo)
|
||||
if self.cf_t > self.CF_WINDOW or self.cf_vmax >= self.CF_V_MAX or a_target > 0.0 and self.cf_stopping:
|
||||
self.cf_t, self.cf_stopping = None, False
|
||||
return False
|
||||
if a_target <= self.CF_TRIGGER:
|
||||
self.cf_stopping = True
|
||||
return self.cf_stopping
|
||||
|
||||
def update(self, prev_state, state, CS, a_target, prev_accel, stock_accel, accel_limits, has_lead=False, pitch=None,
|
||||
a_long=None):
|
||||
if prev_state == LongCtrlState.stopping and state == LongCtrlState.pid and CS.standstill:
|
||||
self.go_t += DT_CTRL
|
||||
if self.go_t < self.STOPPING_EXIT_DEBOUNCE:
|
||||
state = LongCtrlState.stopping
|
||||
else:
|
||||
self.go_t = 0.0
|
||||
|
||||
self.standstill_t = self.standstill_t + DT_CTRL if CS.standstill else 0.0
|
||||
self.engaged_t = 0.0 if state == LongCtrlState.off else self.engaged_t + DT_CTRL
|
||||
self.stopping_t = self.stopping_t + DT_CTRL if state == LongCtrlState.stopping else 0.0
|
||||
if state != LongCtrlState.stopping:
|
||||
self.stopped_once = False
|
||||
elif CS.standstill:
|
||||
self.stopped_once = True
|
||||
|
||||
creeping = self.stopped_once and not CS.standstill and CS.vEgo > self.CREEP_V_MIN
|
||||
self.creep_t = self.creep_t + DT_CTRL if creeping else 0.0
|
||||
cf_stop = self._update_creep_follow(state, CS, a_target)
|
||||
stop_confirmed = self.stop_confirm.update(CS.standstill, CS.vEgo, a_long)
|
||||
|
||||
# end-of-stop window: rolling below BLEND_V with a plan that is braking to a stop
|
||||
rolling = not CS.standstill and not self.stopped_once
|
||||
if state == LongCtrlState.off or not rolling or CS.vEgo >= self.BLEND_V or a_target > 0.0:
|
||||
self.end_active = False
|
||||
elif not self.end_active and a_target <= self.END_PLAN_MIN:
|
||||
self.end_active = True
|
||||
self.a_entry = min(prev_accel, self.END_HI) # the line starts where the request is now, at least END_HI
|
||||
|
||||
if state != LongCtrlState.stopping:
|
||||
if cf_stop:
|
||||
return state, float(np.clip(self._cf_request(a_target, prev_accel), accel_limits[0], accel_limits[1]))
|
||||
if self.end_active:
|
||||
return state, float(np.clip(self._end_request(a_target, prev_accel, CS.vEgo), accel_limits[0], accel_limits[1]))
|
||||
# coming off the hold: linear, slow through the hand-over band (see RELEASE_*)
|
||||
if prev_state == LongCtrlState.stopping and state == LongCtrlState.pid and prev_accel < self.RELEASE_BAND_HI:
|
||||
self.releasing = True
|
||||
if self.releasing:
|
||||
if stock_accel <= prev_accel or prev_accel >= self.RELEASE_BAND_HI or state == LongCtrlState.off:
|
||||
self.releasing = False
|
||||
return state, stock_accel
|
||||
rate = self.RELEASE_FAST if prev_accel < self.RELEASE_BAND_LO else self.RELEASE_SLOW
|
||||
return state, float(min(stock_accel, prev_accel + rate * DT_CTRL))
|
||||
return state, stock_accel
|
||||
|
||||
output_accel = prev_accel
|
||||
hold_floor = self._hold_floor(pitch, creeping)
|
||||
if output_accel > hold_floor:
|
||||
output_accel = min(output_accel, 0.0)
|
||||
if not CS.standstill and not self.stopped_once and a_target < self.STOPPING_FOLLOW_MIN and a_target > output_accel:
|
||||
output_accel = min(a_target, output_accel + self.STOPPING_FOLLOW_RATE * DT_CTRL)
|
||||
if cf_stop:
|
||||
output_accel = self._cf_request(a_target, prev_accel)
|
||||
elif self.end_active:
|
||||
# still rolling inside the end window: the line (or the plan where it is firmer), never the eased-off curve
|
||||
output_accel = self._end_request(a_target, prev_accel, CS.vEgo)
|
||||
|
||||
hold_delay = self.STANDSTILL_HOLD_DELAY_LEAD if has_lead else self.STANDSTILL_HOLD_DELAY_NO_LEAD
|
||||
if CS.standstill and not has_lead and self.engaged_t < self.ENGAGE_STANDSTILL_GRACE:
|
||||
rate = 0.0 # just engaged at a standstill: no brake pulse before the launch
|
||||
elif self.cf_stopped and CS.standstill and output_accel > self.CF_SETTLE_ACCEL:
|
||||
output_accel = max(self.CF_SETTLE_ACCEL, output_accel - self.CF_SETTLE_JERK * DT_CTRL) # H: a little brake now
|
||||
rate = 0.0
|
||||
elif self.cf_stopped and CS.standstill:
|
||||
rate = self.CF_HOLD_RATE # H: then the rest of the hold, slowly
|
||||
elif self.standstill_t >= hold_delay or stop_confirmed:
|
||||
rate = self.STANDSTILL_HOLD_RATE
|
||||
elif creeping and output_accel > self.REROLL_ACCEL:
|
||||
output_accel = max(self.REROLL_ACCEL, output_accel - self.REROLL_JERK * DT_CTRL) # B: moving again - hold now
|
||||
rate = 0.0
|
||||
elif creeping:
|
||||
rate = min(self.STOPPING_DECEL_RATE + self.CREEP_RATE_GROWTH * self.creep_t, self.CREEP_RATE_MAX)
|
||||
elif self.stopping_t >= self.STOPPING_FREEZE_MAX:
|
||||
rate = self.STOPPING_DECEL_RATE
|
||||
else:
|
||||
rate = 0.0
|
||||
if hold_floor != self.stop_accel:
|
||||
output_accel = max(output_accel - rate * DT_CTRL, min(hold_floor, output_accel)) # C: stop at the flat hold
|
||||
else:
|
||||
output_accel -= rate * DT_CTRL
|
||||
|
||||
return state, float(np.clip(output_accel, accel_limits[0], accel_limits[1]))
|
||||
@@ -0,0 +1,314 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import math
|
||||
from types import SimpleNamespace
|
||||
|
||||
from openpilot.common.realtime import DT_CTRL
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.stopping_controller import StoppingController
|
||||
|
||||
LIMITS = (-3.5, 2.0)
|
||||
PID, STOPPING, OFF = LongCtrlState.pid, LongCtrlState.stopping, LongCtrlState.off
|
||||
END_LO, END_HI = StoppingController.END_LO, StoppingController.END_HI
|
||||
|
||||
|
||||
def cs(v_kph, standstill=False):
|
||||
return SimpleNamespace(vEgo=v_kph / 3.6, standstill=standstill)
|
||||
|
||||
|
||||
def run(sc, state, car, a_target, prev, secs, stock=None):
|
||||
"""step the controller for secs with a fixed plan; returns the last output"""
|
||||
out = prev
|
||||
for _ in range(int(secs / DT_CTRL)):
|
||||
_, out = sc.update(state, state, car, a_target, out, a_target if stock is None else stock, LIMITS, has_lead=True)
|
||||
return out
|
||||
|
||||
|
||||
def blend(sc, v_kph):
|
||||
return sc._blend(v_kph / 3.6)
|
||||
|
||||
|
||||
class TestEndOfStop(OpenpilotTestCase):
|
||||
def test_the_line_runs_from_the_entry_request_through_5kph_to_the_rest_value(self):
|
||||
sc = StoppingController(-2.0)
|
||||
out = run(sc, PID, cs(9.9), -1.20, -1.20, 0.2)
|
||||
assert sc.end_active and abs(sc.a_entry - (-1.20)) < 1e-6
|
||||
# 7.5 km/h: halfway between the entry request and END_HI; the planner has eased to -0.30, the line is sent
|
||||
out = run(sc, PID, cs(7.5), -0.30, out, 0.6)
|
||||
assert abs(blend(sc, 7.5) - (-1.20 + (END_HI - -1.20) * 0.5)) < 1e-6
|
||||
assert abs(out - blend(sc, 7.5)) < 1e-6
|
||||
# 5 km/h: END_HI; below it a parabola: steep first, flat toward the rest value; the wheel stop: END_LO
|
||||
out = run(sc, PID, cs(5.0), -0.20, out, 0.4)
|
||||
assert abs(out - END_HI) < 1e-6
|
||||
out = run(sc, PID, cs(2.5), -0.20, out, 0.4)
|
||||
assert abs(out - (-0.48 + -0.60) / 2) < 1e-6
|
||||
assert abs(blend(sc, 4.0) - (-0.65)) < 1e-6 and abs(blend(sc, 1.0) - (-0.30)) < 1e-6
|
||||
assert abs(blend(sc, 0.5) - (-0.30)) < 1e-6 # 1-0 km/h unchanged
|
||||
out = run(sc, PID, cs(0.0), -0.20, out, 0.4)
|
||||
assert abs(out - END_LO) < 1e-6
|
||||
|
||||
def test_the_line_is_a_floor_a_firmer_plan_still_passes(self):
|
||||
sc = StoppingController(-2.0)
|
||||
out = run(sc, PID, cs(9.9), -1.20, -1.20, 0.2)
|
||||
out = run(sc, PID, cs(5.0), -1.10, out, 0.5)
|
||||
assert abs(out - (-1.10)) < 1e-6
|
||||
out = run(sc, PID, cs(2.5), -1.8, out, 0.6)
|
||||
assert abs(out - (-1.8)) < 1e-6
|
||||
|
||||
def test_a_light_plan_at_entry_starts_the_line_at_end_hi(self):
|
||||
sc = StoppingController(-2.0)
|
||||
light = END_HI + 0.1
|
||||
out = run(sc, PID, cs(7.0), light, light, 0.5)
|
||||
assert abs(sc.a_entry - END_HI) < 1e-6
|
||||
assert abs(out - END_HI) < 1e-6
|
||||
# the planner's own curve eases off below 5 km/h: the request follows the line, never the plan
|
||||
out = run(sc, PID, cs(2.5), -0.10, out, 0.5)
|
||||
assert abs(out - blend(sc, 2.5)) < 1e-6
|
||||
|
||||
def test_entry_is_rate_limited(self):
|
||||
sc = StoppingController(-2.0)
|
||||
light = END_HI + 0.2
|
||||
_, out = sc.update(PID, PID, cs(7.0), light, light, light, LIMITS, has_lead=True)
|
||||
assert out > END_HI
|
||||
assert abs(out - (light - StoppingController.END_RATE * DT_CTRL)) < 1e-6
|
||||
|
||||
def test_an_emergency_plan_still_passes(self):
|
||||
sc = StoppingController(-2.0)
|
||||
out = run(sc, PID, cs(2.5), -1.8, -0.40, 0.8)
|
||||
assert abs(out - (-1.8)) < 1e-6
|
||||
|
||||
def test_crawl_follow_is_untouched(self):
|
||||
sc = StoppingController(-2.0)
|
||||
out = run(sc, PID, cs(2.0), -0.10, -0.10, 0.5)
|
||||
assert abs(out - (-0.10)) < 1e-6
|
||||
assert not sc.end_active
|
||||
|
||||
def test_above_blend_v_nothing_changes(self):
|
||||
sc = StoppingController(-2.0)
|
||||
out = run(sc, PID, cs(12.0), -0.40, -0.40, 0.5)
|
||||
assert abs(out - (-0.40)) < 1e-6
|
||||
assert not sc.end_active
|
||||
|
||||
def test_lead_moving_off_releases_the_hold(self):
|
||||
sc = StoppingController(-2.0)
|
||||
run(sc, PID, cs(2.0), -0.60, -0.60, 0.5)
|
||||
assert sc.end_active
|
||||
_, out = sc.update(PID, PID, cs(1.5), 0.50, END_LO, 0.50, LIMITS, has_lead=True)
|
||||
assert not sc.end_active and abs(out - 0.50) < 1e-6
|
||||
|
||||
def test_stopping_state_keeps_the_line_until_the_wheels_stop_then_holds(self):
|
||||
sc = StoppingController(-1.0)
|
||||
run(sc, PID, cs(2.0), END_HI, END_HI, 0.5)
|
||||
# 1 km/h, stopping state, the planner's curve has eased to -0.10: Kumar's lighter-follow would ease the request
|
||||
out = run(sc, STOPPING, cs(1.0), -0.10, blend(sc, 2.0), 0.4)
|
||||
assert abs(out - blend(sc, 1.0)) < 1e-6
|
||||
# wheels stop: the last value of the line is held until the hold delay, then ramps to stopAccel at the hold rate
|
||||
out_at_stop = run(sc, STOPPING, cs(0.0, standstill=True), -0.10, out, 0.9)
|
||||
assert abs(out_at_stop - out) < 1e-6 and END_HI < out_at_stop <= END_LO + 1e-6
|
||||
out_held = run(sc, STOPPING, cs(0.0, standstill=True), -0.10, out_at_stop, 1.0)
|
||||
assert out_held < out_at_stop - 0.3
|
||||
assert out_held >= -1.0 - 1e-6
|
||||
|
||||
|
||||
class TestHoldRelease(OpenpilotTestCase):
|
||||
"""2026-10-04 14:43: leaving the hold is fast while the request is far below zero, then linear and slow through the
|
||||
hand-over band, and the plan is never exceeded."""
|
||||
|
||||
def test_release_is_slow_through_the_handover_band(self):
|
||||
sc = StoppingController(-2.0)
|
||||
out = -2.0
|
||||
t = 0.0
|
||||
t_lo = t_hi = None
|
||||
state = STOPPING # controlsd feeds the returned state back as prev_state (the 0.2 s exit debounce lives there)
|
||||
for _ in range(300):
|
||||
state, out = sc.update(state, PID, cs(0.0, standstill=True), 0.8, out, 0.8, LIMITS, has_lead=True)
|
||||
t += DT_CTRL
|
||||
if t_lo is None and out >= StoppingController.RELEASE_BAND_LO:
|
||||
t_lo = t
|
||||
if t_hi is None and out >= StoppingController.RELEASE_BAND_HI:
|
||||
t_hi = t
|
||||
assert out <= 0.8 + 1e-9
|
||||
assert t_lo is not None and t_lo < 0.6 # 0.2 s debounce, then -2.0 -> -0.6 at the fast rate (0.35 s)
|
||||
assert t_hi is not None and 1.15 < t_hi < 1.3 # then -0.6 -> +0.4 at 1.5 m/s^3 (0.67 s)
|
||||
assert abs(out - 0.8) < 1e-9 and not sc.releasing
|
||||
|
||||
def test_a_plan_that_drops_again_ends_the_release(self):
|
||||
sc = StoppingController(-2.0)
|
||||
state, out = STOPPING, -2.0
|
||||
for _ in range(30): # through the exit debounce
|
||||
state, out = sc.update(state, PID, cs(0.0, standstill=True), 0.5, out, 0.5, LIMITS, has_lead=True)
|
||||
assert sc.releasing and -2.0 < out < 0.5
|
||||
_, out = sc.update(state, PID, cs(0.0, standstill=True), -2.0, out, -2.0, LIMITS, has_lead=True)
|
||||
assert abs(out - (-2.0)) < 1e-9 and not sc.releasing
|
||||
|
||||
|
||||
class TestEngageAtStandstill(OpenpilotTestCase):
|
||||
def _engage(self, has_lead, secs):
|
||||
sc = StoppingController(-2.0)
|
||||
car = cs(0.0, standstill=True)
|
||||
for _ in range(300): # standing in the auto hold, openpilot not engaged
|
||||
sc.update(OFF, OFF, car, 0.0, 0.0, 0.0, LIMITS, has_lead=has_lead)
|
||||
out = 0.0
|
||||
for _ in range(int(secs / DT_CTRL)):
|
||||
_, out = sc.update(STOPPING, STOPPING, car, 0.0, out, 0.0, LIMITS, has_lead=has_lead)
|
||||
return out, sc
|
||||
|
||||
def test_no_brake_pulse_right_after_engaging_without_lead(self):
|
||||
# route 0000010a 23:58:49: the hold ramp reached -0.21 in 0.2 s and the PCM braked 1600 N before the launch
|
||||
out, sc = self._engage(False, 0.5)
|
||||
assert out == 0.0
|
||||
|
||||
def test_the_hold_ramp_follows_after_the_grace(self):
|
||||
out, sc = self._engage(False, StoppingController.ENGAGE_STANDSTILL_GRACE + 0.3)
|
||||
assert out < -0.2
|
||||
|
||||
def test_with_a_lead_the_hold_starts_at_once(self):
|
||||
out, _ = self._engage(True, 0.3)
|
||||
assert out < -0.2
|
||||
|
||||
|
||||
def stand(sc, secs, out=StoppingController.END_LO, pitch=None, a_target=-0.1):
|
||||
state = STOPPING
|
||||
for _ in range(int(secs / DT_CTRL)):
|
||||
state, out = sc.update(state, STOPPING, cs(0.0, standstill=True), a_target, out, a_target, LIMITS, has_lead=True, pitch=pitch)
|
||||
return out
|
||||
|
||||
|
||||
FLAT = math.radians(1.0)
|
||||
SLOPE = math.radians(-4.0)
|
||||
|
||||
|
||||
class TestRerollHold(OpenpilotTestCase):
|
||||
"""route 0000010e 17:38:48: stood at -0.30, rolled at 0.8 km/h for 1.1 s inside the hold delay"""
|
||||
|
||||
def _reroll(self, sc):
|
||||
out = stand(sc, 0.9)
|
||||
state = STOPPING
|
||||
outs = []
|
||||
for _ in range(20):
|
||||
state, out = sc.update(state, STOPPING, cs(0.8), -0.05, out, -0.05, LIMITS, has_lead=True)
|
||||
outs.append(out)
|
||||
return outs
|
||||
|
||||
def test_steps_to_the_reroll_request_within_0_1s(self):
|
||||
outs = self._reroll(StoppingController(-2.0))
|
||||
assert outs[4] <= StoppingController.REROLL_ACCEL + 1e-9 # 0.05 s
|
||||
assert all(b - a >= -StoppingController.REROLL_JERK * DT_CTRL - 1e-9 for a, b in zip(outs, outs[1:], strict=False))
|
||||
|
||||
|
||||
class TestFlatHold(OpenpilotTestCase):
|
||||
def test_flat_hold_stops_at_minus_1_2(self):
|
||||
out = stand(StoppingController(-2.0), 4.0, pitch=FLAT)
|
||||
assert abs(out - StoppingController.FLAT_HOLD_ACCEL) < 1e-9
|
||||
|
||||
def test_slope_or_no_pitch_keeps_stop_accel(self):
|
||||
assert stand(StoppingController(-2.0), 4.0, pitch=SLOPE) <= -2.0 + 1e-6
|
||||
assert stand(StoppingController(-2.0), 4.0, pitch=None) <= -2.0 + 1e-6
|
||||
|
||||
def test_a_creep_on_the_flat_hold_goes_deeper(self):
|
||||
sc = StoppingController(-2.0)
|
||||
out = stand(sc, 4.0, pitch=FLAT)
|
||||
state = STOPPING
|
||||
for _ in range(150):
|
||||
state, out = sc.update(state, STOPPING, cs(0.8), -0.1, out, -0.1, LIMITS, has_lead=True, pitch=FLAT)
|
||||
assert out < StoppingController.FLAT_HOLD_ACCEL - 0.5
|
||||
|
||||
|
||||
class TestCreepFollowStop(OpenpilotTestCase):
|
||||
"""route 0000010d 23:56:59: lead moved ~2 m, the car followed at 2.5 km/h and the brake bit at -0.88"""
|
||||
|
||||
def _creep(self, sc, plan, v_kph=2.0, secs=2.0, out=0.4):
|
||||
stand(sc, 2.0)
|
||||
outs = []
|
||||
for _ in range(int(secs / DT_CTRL)):
|
||||
_, out = sc.update(PID, PID, cs(v_kph), plan, out, plan, LIMITS, has_lead=True)
|
||||
outs.append(out)
|
||||
return outs
|
||||
|
||||
def test_stops_with_minus_0_24_not_the_end_line(self):
|
||||
outs = self._creep(StoppingController(-2.0), -0.40)
|
||||
assert outs[0] <= 0.0 # a positive request is dropped at once
|
||||
assert abs(outs[-1] - StoppingController.CF_END) < 1e-9
|
||||
steps = [a - b for a, b in zip(outs, outs[1:], strict=False)]
|
||||
assert max(steps) <= StoppingController.CF_RATE * DT_CTRL + 1e-9
|
||||
|
||||
def test_a_firm_plan_still_wins(self):
|
||||
outs = self._creep(StoppingController(-2.0), -1.0)
|
||||
assert abs(outs[-1] - (-1.0)) < 1e-9
|
||||
|
||||
def test_light_plan_keeps_minus_0_24_to_the_stop(self):
|
||||
sc = StoppingController(-2.0)
|
||||
outs = self._creep(sc, -0.40, secs=1.0)
|
||||
_, out = sc.update(PID, PID, cs(1.0), -0.10, outs[-1], -0.10, LIMITS, has_lead=True)
|
||||
assert abs(out - StoppingController.CF_END) < 1e-9
|
||||
|
||||
def test_not_after_a_real_launch(self):
|
||||
sc = StoppingController(-2.0)
|
||||
stand(sc, 2.0)
|
||||
out = 0.5
|
||||
for _ in range(300):
|
||||
_, out = sc.update(PID, PID, cs(15.0), 0.3, out, 0.3, LIMITS, has_lead=True)
|
||||
_, out = sc.update(PID, PID, cs(6.0), -0.40, out, -0.40, LIMITS, has_lead=True)
|
||||
assert sc.cf_t is None
|
||||
|
||||
def test_lead_moving_off_ends_it(self):
|
||||
sc = StoppingController(-2.0)
|
||||
outs = self._creep(sc, -0.40, secs=0.5)
|
||||
_, out = sc.update(PID, PID, cs(2.0), 0.5, outs[-1], 0.5, LIMITS, has_lead=True)
|
||||
assert out == 0.5 and not sc.cf_stopping
|
||||
|
||||
def test_after_the_creep_stop_a_little_brake_at_once_then_the_hold_slowly(self):
|
||||
"""route 00000112 23:16:57: -0.24 for the 1.0 s hold delay, then -1.2 within ~1 s felt like a second brake"""
|
||||
sc = StoppingController(-2.0)
|
||||
outs = self._creep(sc, -0.40)
|
||||
out, state, trace = outs[-1], STOPPING, []
|
||||
for _ in range(300):
|
||||
state, out = sc.update(state, STOPPING, cs(0.0, standstill=True), -0.05, out, -0.05, LIMITS, has_lead=True,
|
||||
pitch=FLAT)
|
||||
trace.append(out)
|
||||
assert trace[50] <= StoppingController.CF_SETTLE_ACCEL + 1e-9 # -0.6 within 0.5 s, no hold delay
|
||||
assert trace[10] > StoppingController.CF_SETTLE_ACCEL # but gradually
|
||||
assert abs(trace[150] - (StoppingController.CF_SETTLE_ACCEL - 1.0 * StoppingController.CF_HOLD_RATE)) < 0.02
|
||||
assert abs(trace[-1] - StoppingController.FLAT_HOLD_ACCEL) < 1e-9 # the flat hold after ~2.5 s
|
||||
steps = [a - b for a, b in zip(trace, trace[1:], strict=False)]
|
||||
assert max(steps) <= StoppingController.CF_SETTLE_JERK * DT_CTRL + 1e-9
|
||||
|
||||
def test_an_ordinary_stop_keeps_the_hold_delay(self):
|
||||
sc = StoppingController(-2.0)
|
||||
out = stand(sc, 0.9, pitch=FLAT)
|
||||
assert abs(out - END_LO) < 1e-9
|
||||
|
||||
|
||||
class TestConfirmedStopEarlyHold(OpenpilotTestCase):
|
||||
"""hold 0.2 s after a CONFIRMED stop (standstill flag + the body's deceleration collapsing), else the 1.0 s delay"""
|
||||
|
||||
def _stop(self, sc, collapse):
|
||||
out, state = -0.40, PID
|
||||
for k in range(100): # rolling to the stop at -0.45 m/s^2 (body)
|
||||
state, out = sc.update(state, STOPPING, cs(2.0 - 0.019 * k), -0.3, out, -0.3, LIMITS, has_lead=True, pitch=FLAT, a_long=-0.45)
|
||||
trace = []
|
||||
for k in range(150): # standing: the body pitches back (deceleration collapses) after 0.1 s, or not
|
||||
a = -0.05 if (collapse and k >= 10) else -0.45
|
||||
state, out = sc.update(state, STOPPING, cs(0.0, True), -0.05, out, -0.05, LIMITS, has_lead=True, pitch=FLAT, a_long=a)
|
||||
trace.append(out)
|
||||
return trace
|
||||
|
||||
def test_confirmed_stop_holds_after_0_2s(self):
|
||||
sc = StoppingController(-2.0)
|
||||
tr = self._stop(sc, True)
|
||||
assert tr[25] == tr[0] # 0.1 s to the pitch-back + 0.2 s: nothing yet at 0.25 s
|
||||
assert tr[40] < tr[0] - 0.05 # holding by 0.4 s
|
||||
|
||||
def test_no_confirmation_keeps_the_1s_delay(self):
|
||||
sc = StoppingController(-2.0)
|
||||
tr = self._stop(sc, False)
|
||||
assert tr[90] == tr[0] and tr[110] < tr[0]
|
||||
|
||||
def test_no_pose_keeps_the_1s_delay(self):
|
||||
sc = StoppingController(-2.0)
|
||||
assert not sc.stop_confirm.update(True, 0.0, None)
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -10,29 +10,29 @@ void live_update_32(double *in_x, double *in_P, double *in_z, double *in_R, doub
|
||||
void live_update_13(double *in_x, double *in_P, double *in_z, double *in_R, double *in_ea);
|
||||
void live_update_14(double *in_x, double *in_P, double *in_z, double *in_R, double *in_ea);
|
||||
void live_update_33(double *in_x, double *in_P, double *in_z, double *in_R, double *in_ea);
|
||||
void live_H(double *in_vec, double *out_8536236811475430067);
|
||||
void live_err_fun(double *nom_x, double *delta_x, double *out_4614868058485758139);
|
||||
void live_inv_err_fun(double *nom_x, double *true_x, double *out_8573554928028298681);
|
||||
void live_H_mod_fun(double *state, double *out_6128852447133223565);
|
||||
void live_f_fun(double *state, double dt, double *out_7195534889995166041);
|
||||
void live_F_fun(double *state, double dt, double *out_9012508306389018670);
|
||||
void live_h_4(double *state, double *unused, double *out_2657657745771735915);
|
||||
void live_H_4(double *state, double *unused, double *out_1457735078816209632);
|
||||
void live_h_9(double *state, double *unused, double *out_181525824636861445);
|
||||
void live_H_9(double *state, double *unused, double *out_8744954014080657102);
|
||||
void live_h_10(double *state, double *unused, double *out_2940864223365932608);
|
||||
void live_H_10(double *state, double *unused, double *out_6591157835936482265);
|
||||
void live_h_12(double *state, double *unused, double *out_830811219903448800);
|
||||
void live_H_12(double *state, double *unused, double *out_4923523298226523364);
|
||||
void live_h_35(double *state, double *unused, double *out_1177115113523305926);
|
||||
void live_H_35(double *state, double *unused, double *out_2177960265901509655);
|
||||
void live_h_32(double *state, double *unused, double *out_8970203849717307468);
|
||||
void live_H_32(double *state, double *unused, double *out_5493453269239321848);
|
||||
void live_h_13(double *state, double *unused, double *out_1337448462981849436);
|
||||
void live_H_13(double *state, double *unused, double *out_2400089242629978824);
|
||||
void live_h_14(double *state, double *unused, double *out_181525824636861445);
|
||||
void live_H_14(double *state, double *unused, double *out_8744954014080657102);
|
||||
void live_h_33(double *state, double *unused, double *out_3078698583527751553);
|
||||
void live_H_33(double *state, double *unused, double *out_972596738737347949);
|
||||
void live_H(double *in_vec, double *out_5926322025614992030);
|
||||
void live_err_fun(double *nom_x, double *delta_x, double *out_5043251176595415602);
|
||||
void live_inv_err_fun(double *nom_x, double *true_x, double *out_3360739843129809304);
|
||||
void live_H_mod_fun(double *state, double *out_8279386211598339514);
|
||||
void live_f_fun(double *state, double dt, double *out_6890687714462639744);
|
||||
void live_F_fun(double *state, double dt, double *out_7621965816561569981);
|
||||
void live_h_4(double *state, double *unused, double *out_3851734767229128263);
|
||||
void live_H_4(double *state, double *unused, double *out_5878769767090540838);
|
||||
void live_h_9(double *state, double *unused, double *out_2694197172040937469);
|
||||
void live_H_9(double *state, double *unused, double *out_1408449168173906632);
|
||||
void live_h_10(double *state, double *unused, double *out_7033125878671447195);
|
||||
void live_H_10(double *state, double *unused, double *out_65675330875969869);
|
||||
void live_h_12(double *state, double *unused, double *out_8799002316774637565);
|
||||
void live_H_12(double *state, double *unused, double *out_6186715929576277782);
|
||||
void live_h_35(double *state, double *unused, double *out_2868906334315872882);
|
||||
void live_H_35(double *state, double *unused, double *out_8932278961901291491);
|
||||
void live_h_32(double *state, double *unused, double *out_9199462926327450350);
|
||||
void live_H_32(double *state, double *unused, double *out_6730806947389842532);
|
||||
void live_h_13(double *state, double *unused, double *out_3826583407417889778);
|
||||
void live_H_13(double *state, double *unused, double *out_3672088104010170074);
|
||||
void live_h_14(double *state, double *unused, double *out_2694197172040937469);
|
||||
void live_H_14(double *state, double *unused, double *out_1408449168173906632);
|
||||
void live_h_33(double *state, double *unused, double *out_6802426037572342548);
|
||||
void live_H_33(double *state, double *unused, double *out_7684478583555780967);
|
||||
void live_predict(double *in_x, double *in_P, double *in_Q, double dt);
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
from collections import deque
|
||||
from collections.abc import Callable
|
||||
from dataclasses import asdict, dataclass
|
||||
import math
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from openpilot.cereal import log, messaging
|
||||
from opendbc.car.interfaces import ACCEL_MAX, ACCEL_MIN
|
||||
from openpilot.common.realtime import DT_CTRL, DT_MDL, Ratekeeper
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
from openpilot.selfdrive.controls.lib.longcontrol import LongControl, LongCtrlState
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanner
|
||||
from openpilot.selfdrive.controls.radard import _LEAD_ACCEL_TAU
|
||||
from openpilot.selfdrive.test.longitudinal_maneuvers.plant import Plant, PlannerSM
|
||||
|
||||
|
||||
LeadObservation = dict[str, Any]
|
||||
LeadObservationFn = Callable[[float, str, LeadObservation], LeadObservation | None]
|
||||
ModelActionFn = Callable[[float, float, float], tuple[float, bool]]
|
||||
EgoObservationFn = Callable[[float, float, float], tuple[float, float]]
|
||||
ModelPlanFn = Callable[[float, float, float], list[float]]
|
||||
ModelMetaFn = Callable[[float], tuple[list[float], bool, float]]
|
||||
LeadFutureProbsFn = Callable[[float], tuple[float, float, float]]
|
||||
PositionYFn = Callable[[float], list[float]]
|
||||
ExperimentalModeFn = Callable[[float], bool]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ActuatorModel:
|
||||
planner_delay: float
|
||||
transport_delay: float
|
||||
actuator_lag: float
|
||||
command_rate_limit: float
|
||||
stopping_acceleration: float
|
||||
standstill_breakaway_acceleration: float
|
||||
standstill_breakaway_time: float
|
||||
|
||||
def __post_init__(self):
|
||||
nonnegative_fields = {
|
||||
"planner_delay": self.planner_delay,
|
||||
"transport_delay": self.transport_delay,
|
||||
"actuator_lag": self.actuator_lag,
|
||||
"standstill_breakaway_acceleration": self.standstill_breakaway_acceleration,
|
||||
"standstill_breakaway_time": self.standstill_breakaway_time,
|
||||
}
|
||||
if any(not math.isfinite(value) or value < 0.0 for value in nonnegative_fields.values()):
|
||||
raise ValueError(f"ActuatorModel fields must be finite and non-negative: {nonnegative_fields}")
|
||||
if not math.isfinite(self.command_rate_limit) or self.command_rate_limit <= 0.0:
|
||||
raise ValueError("command_rate_limit must be finite and positive")
|
||||
if not math.isfinite(self.stopping_acceleration) or self.stopping_acceleration > 0.0:
|
||||
raise ValueError("stopping_acceleration must be finite and non-positive")
|
||||
|
||||
|
||||
# Conservative Prius TSS2 actuator model.
|
||||
PRIUS_TSS2_ROUTE_MODEL = ActuatorModel(
|
||||
planner_delay=0.05,
|
||||
transport_delay=0.0,
|
||||
actuator_lag=0.20,
|
||||
command_rate_limit=4.0,
|
||||
stopping_acceleration=-2.0,
|
||||
standstill_breakaway_acceleration=1.0,
|
||||
standstill_breakaway_time=0.05,
|
||||
)
|
||||
|
||||
|
||||
class PlantSP(Plant):
|
||||
"""Closed-loop plant with configurable observations and actuator response."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
lead_relevancy=False,
|
||||
speed=0.0,
|
||||
distance_lead=2.0,
|
||||
enabled=True,
|
||||
only_lead2=False,
|
||||
only_radar=False,
|
||||
e2e=False,
|
||||
personality=0,
|
||||
force_decel=False,
|
||||
lead_observation_fn: LeadObservationFn | None = None,
|
||||
model_action_fn: ModelActionFn | None = None,
|
||||
ego_observation_fn: EgoObservationFn | None = None,
|
||||
model_plan_fn: ModelPlanFn | None = None,
|
||||
model_meta_fn: ModelMetaFn | None = None,
|
||||
lead_future_probs_fn: LeadFutureProbsFn | None = None,
|
||||
position_y_fn: PositionYFn | None = None,
|
||||
experimental_mode_fn: ExperimentalModeFn | None = None,
|
||||
actuator_delay: float | None = None,
|
||||
actuator_lag: float = 0.0,
|
||||
actuator_model: ActuatorModel | None = None,
|
||||
run_long_control: bool = False,
|
||||
):
|
||||
if actuator_delay is not None and (not math.isfinite(actuator_delay) or actuator_delay < 0.0):
|
||||
raise ValueError("actuator_delay must be finite and non-negative")
|
||||
if not math.isfinite(actuator_lag) or actuator_lag < 0.0:
|
||||
raise ValueError("actuator_lag must be finite and non-negative")
|
||||
|
||||
self.rate = 1.0 / DT_MDL
|
||||
|
||||
if not Plant.messaging_initialized:
|
||||
Plant.radar = messaging.pub_sock('radarState')
|
||||
Plant.controls_state = messaging.pub_sock('controlsState')
|
||||
Plant.selfdrive_state = messaging.pub_sock('selfdriveState')
|
||||
Plant.car_state = messaging.pub_sock('carState')
|
||||
Plant.plan = messaging.sub_sock('longitudinalPlan')
|
||||
Plant.messaging_initialized = True
|
||||
|
||||
self.v_lead_prev = 0.0
|
||||
|
||||
self.distance = 0.0
|
||||
self.speed = speed
|
||||
self.should_stop = False
|
||||
self.acceleration = 0.0
|
||||
self.a_target = 0.0
|
||||
self.actuator_command = 0.0
|
||||
self.applied_actuator_command = 0.0
|
||||
self.breakaway_confirmed = False
|
||||
self._breakaway_timer = 0.0
|
||||
|
||||
# lead car
|
||||
self.lead_relevancy = lead_relevancy
|
||||
self.distance_lead = distance_lead
|
||||
self.enabled = enabled
|
||||
self.only_lead2 = only_lead2
|
||||
self.only_radar = only_radar
|
||||
self.e2e = e2e
|
||||
self.personality = personality
|
||||
self.force_decel = force_decel
|
||||
self.lead_observation_fn = lead_observation_fn
|
||||
self.model_action_fn = model_action_fn
|
||||
self.ego_observation_fn = ego_observation_fn
|
||||
self.model_plan_fn = model_plan_fn
|
||||
self.model_meta_fn = model_meta_fn
|
||||
self.lead_future_probs_fn = lead_future_probs_fn
|
||||
self.position_y_fn = position_y_fn
|
||||
self.experimental_mode_fn = experimental_mode_fn
|
||||
self.actuator_model = actuator_model
|
||||
self.actuator_delay = actuator_model.planner_delay if actuator_model is not None else actuator_delay
|
||||
self.transport_delay = actuator_model.transport_delay if actuator_model is not None else actuator_delay
|
||||
self.actuator_lag = actuator_model.actuator_lag if actuator_model is not None else actuator_lag
|
||||
self.publish_realized_a_ego = any((lead_observation_fn is not None, model_action_fn is not None, ego_observation_fn is not None,
|
||||
actuator_delay is not None, actuator_lag > 0.0, actuator_model is not None, run_long_control))
|
||||
|
||||
self.rk = Ratekeeper(self.rate, print_delay_threshold=100.0)
|
||||
self.ts = 1.0 / self.rate
|
||||
time.sleep(0.1)
|
||||
self.sm = messaging.SubMaster(['longitudinalPlan'])
|
||||
|
||||
from opendbc.car.honda.values import CAR
|
||||
from opendbc.car.honda.interface import CarInterface
|
||||
|
||||
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
|
||||
if self.actuator_delay is not None:
|
||||
CP.longitudinalActuatorDelay = self.actuator_delay
|
||||
CP_SP = CarInterface.get_non_essential_params_sp(CP, CAR.HONDA_CIVIC)
|
||||
self.planner = LongitudinalPlanner(CP, CP_SP, init_v=self.speed)
|
||||
self.long_control = LongControl(CP, CP_SP) if run_long_control else None
|
||||
|
||||
if self.actuator_model is not None and self.speed >= 0.01:
|
||||
self.breakaway_confirmed = True
|
||||
self.integration_dt = DT_CTRL if run_long_control else self.ts
|
||||
delay_steps = 0 if self.transport_delay is None else round(self.transport_delay / self.integration_dt)
|
||||
self._actuator_delay_queue = deque([self.acceleration] * delay_steps)
|
||||
|
||||
@staticmethod
|
||||
def _lead_message(observation: LeadObservation):
|
||||
lead = log.RadarState.LeadData.new_message()
|
||||
for field, value in observation.items():
|
||||
setattr(lead, field, value)
|
||||
return lead
|
||||
|
||||
def _observe_lead(self, lead_name: str, truth: LeadObservation, present_by_default: bool) -> LeadObservation | None:
|
||||
if self.lead_observation_fn is None:
|
||||
return dict(truth) if present_by_default else None
|
||||
|
||||
observed = self.lead_observation_fn(self.current_time, lead_name, dict(truth))
|
||||
if observed is None:
|
||||
return None
|
||||
|
||||
complete_observation = dict(truth)
|
||||
complete_observation.update(observed)
|
||||
return complete_observation
|
||||
|
||||
def _update_actuator(self, command: float) -> tuple[float, float]:
|
||||
if self._actuator_delay_queue:
|
||||
self._actuator_delay_queue.append(command)
|
||||
delayed_command = self._actuator_delay_queue.popleft()
|
||||
else:
|
||||
delayed_command = command
|
||||
|
||||
if self.actuator_model is not None:
|
||||
max_command_delta = self.actuator_model.command_rate_limit * self.integration_dt
|
||||
self.applied_actuator_command = float(np.clip(delayed_command,
|
||||
self.applied_actuator_command - max_command_delta,
|
||||
self.applied_actuator_command + max_command_delta))
|
||||
|
||||
if self.speed < 0.01:
|
||||
if self.applied_actuator_command <= 0.0:
|
||||
self.breakaway_confirmed = False
|
||||
self._breakaway_timer = 0.0
|
||||
elif not self.breakaway_confirmed:
|
||||
breakaway_ready = self.applied_actuator_command + 1e-9 >= self.actuator_model.standstill_breakaway_acceleration
|
||||
if breakaway_ready:
|
||||
self._breakaway_timer += self.integration_dt
|
||||
else:
|
||||
self._breakaway_timer = 0.0
|
||||
|
||||
self.breakaway_confirmed = breakaway_ready and self._breakaway_timer + 1e-9 >= self.actuator_model.standstill_breakaway_time
|
||||
if not self.breakaway_confirmed:
|
||||
self.acceleration = 0.0
|
||||
return delayed_command, self.acceleration
|
||||
else:
|
||||
self.breakaway_confirmed = True
|
||||
|
||||
response_command = self.applied_actuator_command
|
||||
else:
|
||||
self.applied_actuator_command = delayed_command
|
||||
response_command = delayed_command
|
||||
|
||||
if self.actuator_lag > 0.0:
|
||||
alpha = 1.0 - math.exp(-self.integration_dt / self.actuator_lag)
|
||||
self.acceleration += alpha * (response_command - self.acceleration)
|
||||
else:
|
||||
self.acceleration = response_command
|
||||
return delayed_command, self.acceleration
|
||||
|
||||
def _integrate_ego(self, dt: float, stop_at_standstill: bool = False) -> None:
|
||||
self.speed += self.acceleration * dt
|
||||
if self.speed <= 0.0 or stop_at_standstill and self.speed < 0.01 and self.actuator_command <= 0.0:
|
||||
self.speed = self.acceleration = 0.0
|
||||
self.distance += self.speed * dt
|
||||
|
||||
def step(self, v_lead=0.0, prob_lead=1.0, v_cruise=50.0, pitch=0.0, prob_throttle=1.0):
|
||||
# ******** publish a fake model going straight and fake calibration ********
|
||||
# note that this is worst case for MPC, since model will delay long mpc by one time step
|
||||
radar = messaging.new_message('radarState')
|
||||
control = messaging.new_message('controlsState')
|
||||
ss = messaging.new_message('selfdriveState')
|
||||
car_state = messaging.new_message('carState')
|
||||
vehicle_parameters = messaging.new_message('vehicleParameters')
|
||||
car_control = messaging.new_message('carControl')
|
||||
model = messaging.new_message('modelV2')
|
||||
car_state_sp = messaging.new_message('carStateSP')
|
||||
live_map_data_sp = messaging.new_message('liveMapDataSP')
|
||||
gps_data = messaging.new_message('gpsLocation')
|
||||
a_lead = (v_lead - self.v_lead_prev) / self.ts
|
||||
self.v_lead_prev = v_lead
|
||||
|
||||
if self.lead_relevancy:
|
||||
d_rel = np.maximum(0.0, self.distance_lead - self.distance)
|
||||
v_rel = v_lead - self.speed
|
||||
if self.only_radar:
|
||||
status = True
|
||||
elif prob_lead > 0.5:
|
||||
status = True
|
||||
else:
|
||||
status = False
|
||||
else:
|
||||
d_rel = 200.0
|
||||
v_rel = 0.0
|
||||
prob_lead = 0.0
|
||||
status = False
|
||||
|
||||
truth_lead: LeadObservation = {
|
||||
"dRel": float(d_rel),
|
||||
"yRel": 0.0,
|
||||
"vRel": float(v_rel),
|
||||
"vLead": float(v_lead),
|
||||
"vLeadK": float(v_lead),
|
||||
"aLeadK": float(a_lead),
|
||||
"present": bool(status),
|
||||
# TODO use real radard logic for this
|
||||
"aLeadTau": float(_LEAD_ACCEL_TAU),
|
||||
"modelProb": float(prob_lead),
|
||||
"radar": bool(self.only_radar),
|
||||
"radarTrackId": -1,
|
||||
}
|
||||
lead_one_observation = self._observe_lead("leadOne", truth_lead, not self.only_lead2)
|
||||
lead_two_observation = self._observe_lead("leadTwo", truth_lead, True)
|
||||
if lead_one_observation is not None:
|
||||
radar.radarState.leadOne = self._lead_message(lead_one_observation)
|
||||
if lead_two_observation is not None:
|
||||
radar.radarState.leadTwo = self._lead_message(lead_two_observation)
|
||||
|
||||
# Simulate model predicting slightly faster speed
|
||||
# this is to ensure lead policy is effective when model
|
||||
# does not predict slowdown in e2e mode
|
||||
position = log.XYZTData.new_message()
|
||||
position.x = [float(x) for x in (self.speed + 0.5) * np.array(ModelConstants.T_IDXS)]
|
||||
if self.position_y_fn is None:
|
||||
position.y = [0.0] * len(ModelConstants.T_IDXS)
|
||||
else:
|
||||
position.y = [float(y) for y in self.position_y_fn(self.current_time)]
|
||||
model.modelV2.position = position
|
||||
if self.model_action_fn is None:
|
||||
model_acceleration, model_should_stop = self.acceleration + 0.5, False
|
||||
else:
|
||||
model_acceleration, model_should_stop = self.model_action_fn(self.current_time, self.speed, self.acceleration)
|
||||
model.modelV2.action.desiredAcceleration = float(model_acceleration)
|
||||
model.modelV2.action.shouldStop = bool(model_should_stop)
|
||||
velocity = log.XYZTData.new_message()
|
||||
if self.model_plan_fn is None:
|
||||
velocity_plan = [float(x) for x in (self.speed + 0.5) * np.ones_like(ModelConstants.T_IDXS)]
|
||||
velocity_plan[0] = float(self.speed) # always start at current speed
|
||||
else:
|
||||
velocity_plan = [float(x) for x in self.model_plan_fn(self.current_time, self.speed, self.acceleration)]
|
||||
velocity.x = velocity_plan
|
||||
model.modelV2.velocity = velocity
|
||||
acceleration = log.XYZTData.new_message()
|
||||
acceleration.x = [float(x) for x in np.zeros_like(ModelConstants.T_IDXS)]
|
||||
model.modelV2.acceleration = acceleration
|
||||
model.modelV2.meta.disengagePredictions.gasPressProbs = [float(prob_throttle) for _ in range(6)]
|
||||
if self.model_meta_fn is None:
|
||||
brake3_probs, hard_brake_predicted, frame_drop_perc = [0.0] * 5, False, 0.0
|
||||
else:
|
||||
brake3_probs, hard_brake_predicted, frame_drop_perc = self.model_meta_fn(self.current_time)
|
||||
model.modelV2.meta.disengagePredictions.brake3MetersPerSecondSquaredProbs = [float(p) for p in brake3_probs]
|
||||
model.modelV2.meta.hardBrakePredicted = bool(hard_brake_predicted)
|
||||
model.modelV2.frameDropPerc = float(frame_drop_perc)
|
||||
if self.lead_future_probs_fn is not None:
|
||||
model.modelV2.init('leadsV3', 3)
|
||||
lead_future_probs = self.lead_future_probs_fn(self.current_time)
|
||||
for i, (prob, prob_time) in enumerate(zip(lead_future_probs, (0.0, 2.0, 4.0), strict=True)):
|
||||
model.modelV2.leadsV3[i].prob = float(prob)
|
||||
model.modelV2.leadsV3[i].probTime = prob_time
|
||||
|
||||
control.controlsState.longControlState = self.long_control.long_control_state if self.long_control is not None else (
|
||||
LongCtrlState.pid if self.enabled else LongCtrlState.off)
|
||||
ss.selfdriveState.experimentalMode = self.e2e if self.experimental_mode_fn is None else bool(self.experimental_mode_fn(self.current_time))
|
||||
ss.selfdriveState.personality = self.personality
|
||||
control.controlsState.forceDecel = self.force_decel
|
||||
true_v_ego = self.speed
|
||||
true_a_ego = self.acceleration
|
||||
published_v_ego = true_v_ego
|
||||
published_a_ego = true_a_ego if self.publish_realized_a_ego else 0.0
|
||||
if self.ego_observation_fn is not None:
|
||||
published_v_ego, published_a_ego = self.ego_observation_fn(self.current_time, true_v_ego, true_a_ego)
|
||||
car_state.carState.vEgo = float(published_v_ego)
|
||||
car_state.carState.aEgo = float(published_a_ego)
|
||||
car_state.carState.standstill = bool(self.speed < 0.01)
|
||||
car_state.carState.vCruise = float(v_cruise * 3.6)
|
||||
car_control.carControl.orientationNED = [0.0, float(pitch), 0.0]
|
||||
|
||||
# ******** get controlsState messages for plotting ***
|
||||
sm = PlannerSM(self.rk.frame, {
|
||||
'radarState': radar.radarState,
|
||||
'carState': car_state.carState,
|
||||
'carControl': car_control.carControl,
|
||||
'controlsState': control.controlsState,
|
||||
'selfdriveState': ss.selfdriveState,
|
||||
'vehicleParameters': vehicle_parameters.vehicleParameters,
|
||||
'modelV2': model.modelV2,
|
||||
'carStateSP': car_state_sp.carStateSP,
|
||||
'liveMapDataSP': live_map_data_sp.liveMapDataSP,
|
||||
'gpsLocation': gps_data.gpsLocation,
|
||||
})
|
||||
self.planner.update(sm)
|
||||
self.a_target = self.planner.output_a_target
|
||||
if self.long_control is None:
|
||||
self.actuator_command = self.a_target
|
||||
if self.planner.output_should_stop:
|
||||
stopping_acceleration = -0.5 if self.actuator_model is None else self.actuator_model.stopping_acceleration
|
||||
self.actuator_command = min(stopping_acceleration, self.actuator_command)
|
||||
self._update_actuator(self.actuator_command)
|
||||
self._integrate_ego(self.ts)
|
||||
else:
|
||||
for _ in range(round(self.ts / DT_CTRL)):
|
||||
car_state.carState.vEgo = self.speed
|
||||
car_state.carState.aEgo = self.acceleration
|
||||
car_state.carState.standstill = self.speed < 0.01
|
||||
self.actuator_command = self.long_control.update(
|
||||
self.enabled, car_state.carState, self.a_target, self.planner.output_should_stop, (ACCEL_MIN, ACCEL_MAX),
|
||||
)
|
||||
self._update_actuator(self.actuator_command)
|
||||
self._integrate_ego(DT_CTRL, stop_at_standstill=True)
|
||||
self.should_stop = self.planner.output_should_stop
|
||||
fcw = self.planner.fcw
|
||||
self.distance_lead = self.distance_lead + v_lead * self.ts
|
||||
|
||||
# *** radar model ***
|
||||
if self.lead_relevancy:
|
||||
d_rel = np.maximum(0.0, self.distance_lead - self.distance)
|
||||
v_rel = v_lead - self.speed
|
||||
else:
|
||||
d_rel = 200.0
|
||||
v_rel = 0.0
|
||||
|
||||
# print at 5hz
|
||||
# if (self.rk.frame % (self.rate // 5)) == 0:
|
||||
# print("%2.2f sec %6.2f m %6.2f m/s %6.2f m/s2 lead_rel: %6.2f m %6.2f m/s"
|
||||
# % (self.current_time, self.distance, self.speed, self.acceleration, d_rel, v_rel))
|
||||
|
||||
# ******** update prevs ********
|
||||
self.rk.monitor_time()
|
||||
|
||||
return {
|
||||
"distance": self.distance,
|
||||
"speed": self.speed,
|
||||
"acceleration": self.acceleration,
|
||||
"realized_acceleration": self.acceleration,
|
||||
"a_target": self.a_target,
|
||||
"actuator_command": self.actuator_command,
|
||||
"published_a_ego": published_a_ego,
|
||||
"published_v_ego": published_v_ego,
|
||||
"should_stop": self.should_stop,
|
||||
"long_control_state": (int(self.long_control.long_control_state) if self.long_control is not None
|
||||
else control.controlsState.longControlState.raw),
|
||||
"distance_lead": self.distance_lead,
|
||||
"fcw": fcw,
|
||||
"mpc_source": self.planner.mpc.source,
|
||||
"dec_mode": self.planner.dec.mode(),
|
||||
"dec_want_blended": self.planner.dec.want_blended,
|
||||
"dec_signals": asdict(self.planner.dec.signals),
|
||||
"dec_lead_veto": self.planner.dec.lead_veto,
|
||||
"controller_active": self.planner.accel_controller_active,
|
||||
"model_action": {
|
||||
"desiredAcceleration": float(model_acceleration),
|
||||
"shouldStop": bool(model_should_stop),
|
||||
},
|
||||
"truth_lead": dict(truth_lead),
|
||||
"lead_one_observation": None if lead_one_observation is None else dict(lead_one_observation),
|
||||
"lead_two_observation": None if lead_two_observation is None else dict(lead_two_observation),
|
||||
}
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
import numpy as np
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import ENTER_FRAMES, MIN_BLENDED_FRAMES
|
||||
from openpilot.sunnypilot.selfdrive.test.longitudinal_maneuvers.plant import PlantSP
|
||||
|
||||
T_IDXS = np.array(ModelConstants.T_IDXS)
|
||||
|
||||
|
||||
def decel_plan(a):
|
||||
def fn(_current_time, speed, _acceleration):
|
||||
return [float(max(0.0, speed + a * t)) for t in T_IDXS]
|
||||
return fn
|
||||
|
||||
|
||||
def flat_plan():
|
||||
def fn(_current_time, speed, _acceleration):
|
||||
return [float(speed)] * len(T_IDXS)
|
||||
return fn
|
||||
|
||||
|
||||
def alternating_plan(a):
|
||||
def fn(current_time, speed, _acceleration):
|
||||
frame_a = a if round(current_time / DT_MDL) % 2 == 0 else 0.0
|
||||
return [float(max(0.0, speed + frame_a * t)) for t in T_IDXS]
|
||||
return fn
|
||||
|
||||
|
||||
def persistent_lead_probs(_current_time):
|
||||
return (1.0, 0.95, 0.9)
|
||||
|
||||
|
||||
def _run(plant, steps, v_lead=0.0, v_cruise=50.0):
|
||||
solver_failures = 0
|
||||
original_reset = plant.planner.mpc.reset
|
||||
|
||||
def counting_reset(*args, **kw):
|
||||
nonlocal solver_failures
|
||||
if plant.planner.mpc.solution_status != 0:
|
||||
solver_failures += 1
|
||||
return original_reset(*args, **kw)
|
||||
|
||||
plant.planner.mpc.reset = counting_reset
|
||||
return [plant.step(v_lead=v_lead, v_cruise=v_cruise) for _ in range(steps)], solver_failures
|
||||
|
||||
|
||||
def mode_changes(results):
|
||||
modes = [r["dec_mode"] for r in results]
|
||||
return sum(a != b for a, b in zip(modes, modes[1:], strict=False))
|
||||
|
||||
|
||||
class TestDecManeuvers(OpenpilotTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.params = Params()
|
||||
self.params.put_bool("DynamicExperimentalControl", True, block=True)
|
||||
|
||||
def test_s1_lead_clears_with_underlying_slowdown_blends_quickly(self):
|
||||
clear_t = 1.0
|
||||
|
||||
def lead_obs(current_time, _lead_name, truth):
|
||||
return None if current_time >= clear_t else dict(truth)
|
||||
|
||||
plant = PlantSP(lead_relevancy=True, speed=20.0, distance_lead=40.0, e2e=True, only_radar=True,
|
||||
lead_observation_fn=lead_obs, model_plan_fn=decel_plan(-2.5),
|
||||
lead_future_probs_fn=persistent_lead_probs)
|
||||
clear_frame = round(clear_t / DT_MDL)
|
||||
results, _ = _run(plant, steps=clear_frame + ENTER_FRAMES + 5, v_lead=20.0, v_cruise=20.0)
|
||||
|
||||
assert all(r["dec_mode"] == "acc" for r in results[:clear_frame])
|
||||
assert all(r["dec_lead_veto"] for r in results[:clear_frame])
|
||||
post_clear = [r["dec_mode"] for r in results[clear_frame:clear_frame + ENTER_FRAMES + 2]]
|
||||
assert "blended" in post_clear
|
||||
|
||||
def test_s1b_lead_clears_with_no_underlying_slowdown_stays_acc(self):
|
||||
clear_t = 1.0
|
||||
|
||||
def lead_obs(current_time, _lead_name, truth):
|
||||
return None if current_time >= clear_t else dict(truth)
|
||||
|
||||
plant = PlantSP(lead_relevancy=True, speed=20.0, distance_lead=40.0, e2e=True, only_radar=True,
|
||||
lead_observation_fn=lead_obs, model_plan_fn=flat_plan(),
|
||||
lead_future_probs_fn=persistent_lead_probs)
|
||||
clear_frame = round(clear_t / DT_MDL)
|
||||
results, _ = _run(plant, steps=clear_frame + MIN_BLENDED_FRAMES, v_lead=20.0, v_cruise=20.0)
|
||||
|
||||
assert all(r["dec_mode"] == "acc" for r in results)
|
||||
|
||||
def test_s2_steady_highway_following_never_blends(self):
|
||||
v = 80.0 / 3.6
|
||||
plant = PlantSP(lead_relevancy=True, speed=v, distance_lead=40.0, e2e=True, only_radar=True,
|
||||
model_plan_fn=flat_plan(), lead_future_probs_fn=persistent_lead_probs)
|
||||
results, failures = _run(plant, steps=100, v_lead=v, v_cruise=v)
|
||||
|
||||
assert failures <= 1
|
||||
assert all(r["dec_mode"] == "acc" for r in results)
|
||||
|
||||
def test_s3_low_speed_cruise_no_lead_never_blends(self):
|
||||
v = 15.0 / 3.6
|
||||
plant = PlantSP(lead_relevancy=False, speed=v, e2e=True, model_plan_fn=flat_plan())
|
||||
results, _ = _run(plant, steps=100, v_cruise=v)
|
||||
|
||||
assert all(r["dec_mode"] == "acc" for r in results)
|
||||
|
||||
def test_s4_highway_slowdown_without_lead_blends(self):
|
||||
v0 = 110.0 / 3.6
|
||||
a = (70.0 / 3.6 - v0) / 6.0
|
||||
plant = PlantSP(lead_relevancy=False, speed=v0, e2e=True, model_plan_fn=decel_plan(a))
|
||||
results, _ = _run(plant, steps=10, v_cruise=v0)
|
||||
|
||||
assert any(r["dec_mode"] == "blended" for r in results)
|
||||
|
||||
def test_s5_stop_then_depart_with_lead_present_stays_acc_throughout(self):
|
||||
def departing_lead(current_time):
|
||||
return 0.0 if current_time < 1.0 else min(15.0, 3.0 * (current_time - 1.0))
|
||||
|
||||
plant = PlantSP(lead_relevancy=True, speed=0.0, distance_lead=6.0, e2e=True)
|
||||
results = []
|
||||
solver_failures = 0
|
||||
original_reset = plant.planner.mpc.reset
|
||||
|
||||
def counting_reset(*args, **kw):
|
||||
nonlocal solver_failures
|
||||
if plant.planner.mpc.solution_status != 0:
|
||||
solver_failures += 1
|
||||
return original_reset(*args, **kw)
|
||||
|
||||
plant.planner.mpc.reset = counting_reset
|
||||
for _ in range(200):
|
||||
results.append(plant.step(v_lead=departing_lead(plant.current_time), v_cruise=15.0))
|
||||
|
||||
assert solver_failures <= 1
|
||||
assert all(r["dec_mode"] == "acc" for r in results)
|
||||
assert all(r["dec_lead_veto"] for r in results)
|
||||
|
||||
def test_s6_creep_cycles_behind_lead_stay_acc(self):
|
||||
def creep_cycle_lead(current_time):
|
||||
return 1.5 + 1.5 * np.sin(current_time * 2.0)
|
||||
|
||||
plant = PlantSP(lead_relevancy=True, speed=1.0, distance_lead=8.0, e2e=True, only_radar=True,
|
||||
model_plan_fn=flat_plan(), lead_future_probs_fn=persistent_lead_probs)
|
||||
results = [plant.step(v_lead=creep_cycle_lead(plant.current_time), v_cruise=5.0) for _ in range(200)]
|
||||
|
||||
assert all(r["dec_mode"] == "acc" for r in results)
|
||||
|
||||
def test_s7_oscillating_near_threshold_demand_does_not_flap(self):
|
||||
plant = PlantSP(lead_relevancy=False, speed=20.0, e2e=True, model_plan_fn=alternating_plan(-2.5))
|
||||
results, _ = _run(plant, steps=200, v_cruise=20.0)
|
||||
|
||||
assert mode_changes(results) <= 2
|
||||
|
||||
def test_s8_degraded_model_holds_acc_through_a_slowdown(self):
|
||||
def degraded_meta(_current_time):
|
||||
return [0.0] * 5, False, 60.0
|
||||
|
||||
plant = PlantSP(lead_relevancy=False, speed=20.0, e2e=True, model_plan_fn=decel_plan(-3.0), model_meta_fn=degraded_meta)
|
||||
results, _ = _run(plant, steps=30, v_cruise=20.0)
|
||||
|
||||
assert all(r["dec_mode"] == "acc" for r in results)
|
||||
|
||||
def test_s9_curve_exclusion_prevents_false_blend_on_a_bend(self):
|
||||
plant = PlantSP(lead_relevancy=False, speed=20.0, e2e=True, model_plan_fn=decel_plan(-1.0),
|
||||
position_y_fn=lambda _t: [6.0] * len(T_IDXS))
|
||||
results, _ = _run(plant, steps=30, v_cruise=20.0)
|
||||
|
||||
assert all(r["dec_mode"] == "acc" for r in results)
|
||||
|
||||
def test_s11_curve_does_not_interrupt_an_active_hard_stop(self):
|
||||
plant = PlantSP(lead_relevancy=False, speed=20.0, e2e=True, model_plan_fn=decel_plan(-2.5),
|
||||
position_y_fn=lambda _t: [6.0] * len(T_IDXS))
|
||||
results, _ = _run(plant, steps=10, v_cruise=20.0)
|
||||
|
||||
assert all(r["dec_mode"] == "blended" for r in results[ENTER_FRAMES - 1:])
|
||||
|
||||
def test_s10_hard_brake_override_inert_while_lead_present(self):
|
||||
def hard_brake_meta(_current_time):
|
||||
return [0.0] * 5, True, 0.0
|
||||
|
||||
plant = PlantSP(lead_relevancy=True, speed=20.0, distance_lead=40.0, e2e=True, only_radar=True,
|
||||
model_plan_fn=flat_plan(), model_meta_fn=hard_brake_meta, lead_future_probs_fn=persistent_lead_probs)
|
||||
results, _ = _run(plant, steps=10, v_lead=20.0, v_cruise=20.0)
|
||||
|
||||
assert all(r["dec_mode"] == "acc" for r in results)
|
||||
@@ -0,0 +1,164 @@
|
||||
from collections.abc import Callable
|
||||
import math
|
||||
from typing import cast
|
||||
|
||||
from openpilot.common.parameterized import parameterized
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.selfdrive.test.longitudinal_maneuvers.plant import Plant
|
||||
from openpilot.sunnypilot.selfdrive.test.longitudinal_maneuvers.plant import PlantSP
|
||||
|
||||
STOCK_STEP_KEYS = ("distance", "speed", "acceleration", "should_stop", "distance_lead", "fcw")
|
||||
|
||||
|
||||
def departing_lead(current_time: float) -> float:
|
||||
return 0.0 if current_time < 1.0 else min(2.0, 2.0 * (current_time - 1.0))
|
||||
|
||||
|
||||
def stopped_lead(_current_time: float) -> float:
|
||||
return 0.0
|
||||
|
||||
|
||||
PARITY_SCENARIOS = {
|
||||
"approach_stopped_lead": {"lead_relevancy": True, "speed": 15.0, "distance_lead": 60.0, "v_cruise": 20.0, "v_lead": stopped_lead, "steps": 80},
|
||||
"stop_then_depart": {"lead_relevancy": True, "speed": 0.0, "distance_lead": 6.0, "v_cruise": 8.0, "v_lead": departing_lead, "steps": 120},
|
||||
}
|
||||
|
||||
|
||||
def _drive(cls, *, v_cruise: float, v_lead: Callable[[float], float], steps: int, **kwargs):
|
||||
plant = cls(**kwargs)
|
||||
plant.v_lead_prev = v_lead(0.0)
|
||||
solver_failures = 0
|
||||
original_reset = plant.planner.mpc.reset
|
||||
|
||||
def counting_reset(*args, **kw):
|
||||
nonlocal solver_failures
|
||||
if plant.planner.mpc.solution_status != 0:
|
||||
solver_failures += 1
|
||||
return original_reset(*args, **kw)
|
||||
|
||||
plant.planner.mpc.reset = counting_reset
|
||||
results = []
|
||||
for _ in range(steps):
|
||||
lead_speed = v_lead(plant.current_time)
|
||||
result = plant.step(v_lead=lead_speed, v_cruise=v_cruise)
|
||||
results.append((result, plant.planner.mpc.source, plant.planner.output_a_target))
|
||||
return results, solver_failures
|
||||
|
||||
|
||||
class TestPlantSP(OpenpilotTestCase):
|
||||
@parameterized.expand(PARITY_SCENARIOS, names=("scenario",), ids=lambda scenario: scenario)
|
||||
def test_plant_sp_matches_stock_plant_on_shared_kwargs(self, scenario: str):
|
||||
kwargs = dict(PARITY_SCENARIOS[scenario])
|
||||
v_cruise = cast(float, kwargs.pop("v_cruise"))
|
||||
v_lead = cast(Callable[[float], float], kwargs.pop("v_lead"))
|
||||
steps = cast(int, kwargs.pop("steps"))
|
||||
|
||||
stock_results, stock_failures = _drive(Plant, v_cruise=v_cruise, v_lead=v_lead, steps=steps, **kwargs)
|
||||
sp_results, sp_failures = _drive(PlantSP, v_cruise=v_cruise, v_lead=v_lead, steps=steps, **kwargs)
|
||||
|
||||
assert stock_failures == 0, f"stock Plant solver failed {stock_failures} times in {scenario!r}"
|
||||
assert sp_failures == 0, f"PlantSP solver failed {sp_failures} times in {scenario!r}"
|
||||
|
||||
for frame, ((stock_result, stock_source, stock_a_target), (sp_result, sp_source, sp_a_target)) in enumerate(
|
||||
zip(stock_results, sp_results, strict=True),
|
||||
):
|
||||
for key in STOCK_STEP_KEYS:
|
||||
if isinstance(stock_result[key], float):
|
||||
self.assertAlmostEqual(sp_result[key], stock_result[key], msg=f"{scenario} frame {frame} key {key}")
|
||||
else:
|
||||
assert sp_result[key] == stock_result[key], f"{scenario} frame {frame} key {key}"
|
||||
assert sp_source == stock_source, f"{scenario} frame {frame} mpc.source"
|
||||
self.assertAlmostEqual(sp_a_target, stock_a_target, msg=f"{scenario} frame {frame} output_a_target")
|
||||
|
||||
if scenario == "stop_then_depart":
|
||||
departure_frame = round(1.0 / DT_MDL)
|
||||
for results in (stock_results, sp_results):
|
||||
assert all(result["speed"] < 0.01 for result, _, _ in results[:departure_frame])
|
||||
assert results[departure_frame - 1][0]["should_stop"]
|
||||
assert any(not result["should_stop"] for result, _, _ in results[departure_frame:])
|
||||
assert any(result["speed"] > 0.05 for result, _, _ in results[departure_frame:])
|
||||
stock_release = next(frame for frame, (result, _, _) in enumerate(stock_results)
|
||||
if frame >= departure_frame and not result["should_stop"])
|
||||
sp_release = next(frame for frame, (result, _, _) in enumerate(sp_results)
|
||||
if frame >= departure_frame and not result["should_stop"])
|
||||
stock_motion = next(frame for frame, (result, _, _) in enumerate(stock_results)
|
||||
if frame >= departure_frame and result["speed"] > 0.05)
|
||||
sp_motion = next(frame for frame, (result, _, _) in enumerate(sp_results)
|
||||
if frame >= departure_frame and result["speed"] > 0.05)
|
||||
assert sp_release == stock_release
|
||||
assert sp_motion == stock_motion
|
||||
|
||||
def test_full_lead_observation_is_independent_from_truth(self):
|
||||
callback_inputs = []
|
||||
|
||||
def observe_lead(current_time, lead_name, truth):
|
||||
callback_inputs.append((current_time, lead_name, truth))
|
||||
if lead_name == "leadOne":
|
||||
return {
|
||||
"dRel": 12.5,
|
||||
"vRel": -4.0,
|
||||
"vLead": 6.0,
|
||||
"vLeadK": 5.5,
|
||||
"aLeadK": -1.25,
|
||||
"aLeadTau": 0.7,
|
||||
"present": True,
|
||||
"modelProb": 0.9,
|
||||
"radarTrackId": 42,
|
||||
}
|
||||
return None
|
||||
|
||||
plant = PlantSP(lead_relevancy=True, speed=10.0, distance_lead=50.0, lead_observation_fn=observe_lead)
|
||||
result = plant.step(v_lead=8.0)
|
||||
|
||||
assert [entry[1] for entry in callback_inputs] == ["leadOne", "leadTwo"]
|
||||
self.assertAlmostEqual(callback_inputs[0][2]["dRel"], 50.0)
|
||||
self.assertAlmostEqual(result["truth_lead"]["dRel"], 50.0)
|
||||
self.assertAlmostEqual(result["lead_one_observation"]["dRel"], 12.5)
|
||||
assert result["lead_one_observation"]["radarTrackId"] == 42
|
||||
assert result["lead_two_observation"] is None
|
||||
self.assertAlmostEqual(result["distance_lead"], 50.0 + 8.0 * DT_MDL)
|
||||
|
||||
def test_model_action_realized_acceleration_and_source_logging(self):
|
||||
def model_action(current_time, v_ego, a_ego):
|
||||
return -1.25, True
|
||||
|
||||
plant = PlantSP(speed=10.0, e2e=True, force_decel=True, model_action_fn=model_action, actuator_lag=0.5)
|
||||
first = plant.step()
|
||||
second = plant.step()
|
||||
|
||||
assert first["model_action"] == {"desiredAcceleration": -1.25, "shouldStop": True}
|
||||
self.assertAlmostEqual(first["published_a_ego"], 0.0)
|
||||
self.assertAlmostEqual(second["published_a_ego"], first["realized_acceleration"])
|
||||
assert first["acceleration"] == first["realized_acceleration"]
|
||||
assert abs(first["realized_acceleration"]) < abs(first["actuator_command"])
|
||||
assert first["mpc_source"] is not None
|
||||
assert first["dec_mode"] in ("acc", "blended")
|
||||
assert "controller_active" in first
|
||||
assert first["lead_one_observation"] is not None
|
||||
assert first["truth_lead"] == first["lead_one_observation"]
|
||||
|
||||
def test_default_model_action_matches_stock_plant(self):
|
||||
result = PlantSP(speed=10.0).step()
|
||||
|
||||
self.assertAlmostEqual(result["model_action"]["desiredAcceleration"], 0.5)
|
||||
assert not result["model_action"]["shouldStop"]
|
||||
|
||||
def test_configurable_transport_delay_and_first_order_lag(self):
|
||||
plant = PlantSP(speed=10.0, actuator_delay=2 * DT_MDL, actuator_lag=0.2)
|
||||
|
||||
self.assertAlmostEqual(plant.planner.CP.longitudinalActuatorDelay, 2 * DT_MDL)
|
||||
delayed_commands = [plant._update_actuator(-1.0) for _ in range(3)]
|
||||
assert [command for command, _ in delayed_commands[:2]] == [0.0, 0.0]
|
||||
|
||||
expected_acceleration = -(1.0 - math.exp(-DT_MDL / 0.2))
|
||||
assert delayed_commands[2][0] == -1.0
|
||||
self.assertAlmostEqual(delayed_commands[2][1], expected_acceleration)
|
||||
|
||||
@parameterized.expand(
|
||||
[(-0.1, 0.0), (float("nan"), 0.0), (float("inf"), 0.0), (None, -0.1), (None, float("nan")), (None, float("inf"))],
|
||||
names=("delay", "lag"),
|
||||
)
|
||||
def test_invalid_actuator_dynamics(self, delay, lag):
|
||||
with self.assertRaises(ValueError):
|
||||
PlantSP(actuator_delay=delay, actuator_lag=lag)
|
||||
@@ -652,6 +652,53 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "AccelPersonalityEnabled",
|
||||
"widget": "toggle",
|
||||
"title": "Enable Accel Controller",
|
||||
"description": "Lets you choose how sunnypilot starts, catches up, and settles at the cruise speed. Emergency braking and stopping are unchanged.",
|
||||
"visibility": [
|
||||
{
|
||||
"type": "capability",
|
||||
"field": "has_longitudinal_control",
|
||||
"equals": true
|
||||
}
|
||||
],
|
||||
"enablement": [
|
||||
{
|
||||
"type": "capability",
|
||||
"field": "has_longitudinal_control",
|
||||
"equals": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "AccelPersonality",
|
||||
"widget": "multiple_button",
|
||||
"title": "Acceleration Profile",
|
||||
"description": "Eco is gentlest, Normal balances a prompt start with smooth catch-up, and Sport is more responsive.",
|
||||
"options": [
|
||||
{
|
||||
"value": 0,
|
||||
"label": "Eco"
|
||||
},
|
||||
{
|
||||
"value": 1,
|
||||
"label": "Normal"
|
||||
},
|
||||
{
|
||||
"value": 2,
|
||||
"label": "Sport"
|
||||
}
|
||||
],
|
||||
"enablement": [
|
||||
{
|
||||
"type": "capability",
|
||||
"field": "has_longitudinal_control",
|
||||
"equals": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "IntelligentCruiseButtonManagement",
|
||||
"widget": "toggle",
|
||||
@@ -1605,6 +1652,18 @@
|
||||
"widget": "toggle",
|
||||
"title": "Use Metric System",
|
||||
"description": "Display speed in km/h instead of mph."
|
||||
},
|
||||
{
|
||||
"key": "SunnypilotStoppingController",
|
||||
"widget": "toggle",
|
||||
"needs_onroad_cycle": true,
|
||||
"title": "Stopping Controller",
|
||||
"description": "Take over the end of a stop. The request follows the speed down to the wheel stop, then holds the car at a standstill.",
|
||||
"enablement": [
|
||||
{
|
||||
"type": "offroad_only"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -2302,6 +2361,61 @@
|
||||
"title": "Toyota / Lexus Settings",
|
||||
"description": "",
|
||||
"items": [
|
||||
{
|
||||
"key": "ToyotaAutoHold",
|
||||
"widget": "toggle",
|
||||
"needs_onroad_cycle": true,
|
||||
"title": "Toyota: Auto Brake Hold FOR TSS2 HYBRID CARS",
|
||||
"enablement": [
|
||||
{
|
||||
"type": "not_engaged"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "ToyotaBrakeOnset",
|
||||
"widget": "toggle",
|
||||
"needs_onroad_cycle": true,
|
||||
"title": "Toyota: soft brake onset, overshoot limit and regen hand-over",
|
||||
"enablement": [
|
||||
{
|
||||
"type": "not_engaged"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "ToyotaEnhancedBsm",
|
||||
"widget": "toggle",
|
||||
"needs_onroad_cycle": true,
|
||||
"title": "Toyota: Prius TSS2 BSM and some tssp",
|
||||
"enablement": [
|
||||
{
|
||||
"type": "not_engaged"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "ToyotaTSS2Long",
|
||||
"widget": "toggle",
|
||||
"needs_onroad_cycle": true,
|
||||
"title": "Toyota: custom longitudinal for TSS2",
|
||||
"enablement": [
|
||||
{
|
||||
"type": "not_engaged"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "ToyotaDriveMode",
|
||||
"widget": "toggle",
|
||||
"needs_onroad_cycle": true,
|
||||
"title": "Enable drive mode btn link",
|
||||
"enablement": [
|
||||
{
|
||||
"type": "not_engaged"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "ToyotaEnforceStockLongitudinal",
|
||||
"widget": "toggle",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user