mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-08-22 02:33:47 +08:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bc6e823976 | |||
| cee6a6bdac | |||
| 8d9f3971b0 | |||
| 931ebf1f5a | |||
| c783f2225a | |||
| bdda9006fd | |||
| 53e13a7bc0 |
@@ -4,7 +4,6 @@
|
|||||||
[submodule "opendbc"]
|
[submodule "opendbc"]
|
||||||
path = opendbc_repo
|
path = opendbc_repo
|
||||||
url = https://github.com/sunnypilot/opendbc.git
|
url = https://github.com/sunnypilot/opendbc.git
|
||||||
branch = tn
|
|
||||||
[submodule "msgq"]
|
[submodule "msgq"]
|
||||||
path = msgq_repo
|
path = msgq_repo
|
||||||
url = https://github.com/sunnypilot/msgq.git
|
url = https://github.com/sunnypilot/msgq.git
|
||||||
|
|||||||
+1
-1
Submodule opendbc_repo updated: 847ae2fa55...06743dfb39
@@ -203,16 +203,11 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
|
|||||||
aTarget @5 :Float32;
|
aTarget @5 :Float32;
|
||||||
events @6 :List(OnroadEventSP.Event);
|
events @6 :List(OnroadEventSP.Event);
|
||||||
e2eAlerts @7 :E2eAlerts;
|
e2eAlerts @7 :E2eAlerts;
|
||||||
accelController @8 :AccelController;
|
|
||||||
|
|
||||||
struct DynamicExperimentalControl {
|
struct DynamicExperimentalControl {
|
||||||
state @0 :DynamicExperimentalControlState;
|
state @0 :DynamicExperimentalControlState;
|
||||||
enabled @1 :Bool;
|
enabled @1 :Bool;
|
||||||
active @2 :Bool;
|
active @2 :Bool;
|
||||||
decelIntent @3 :Float32;
|
|
||||||
curveDetected @4 :Bool;
|
|
||||||
wantBlended @5 :Bool;
|
|
||||||
leadVeto @6 :Bool;
|
|
||||||
|
|
||||||
enum DynamicExperimentalControlState {
|
enum DynamicExperimentalControlState {
|
||||||
acc @0;
|
acc @0;
|
||||||
@@ -310,18 +305,6 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
|
|||||||
greenLightAlert @0 :Bool;
|
greenLightAlert @0 :Bool;
|
||||||
leadDepartAlert @1 :Bool;
|
leadDepartAlert @1 :Bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AccelController {
|
|
||||||
enabled @0 :Bool;
|
|
||||||
active @1 :Bool;
|
|
||||||
profile @2 :Profile;
|
|
||||||
reserved3 @3 :Void;
|
|
||||||
enum Profile {
|
|
||||||
eco @0;
|
|
||||||
normal @1;
|
|
||||||
sport @2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct OnroadEventSP @0xda96579883444c35 {
|
struct OnroadEventSP @0xda96579883444c35 {
|
||||||
|
|||||||
@@ -187,12 +187,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"StandstillTimer", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"StandstillTimer", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"TrueVEgoUI", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"TrueVEgoUI", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
|
|
||||||
// toyota specific params
|
|
||||||
{"ToyotaAutoHold", {PERSISTENT | BACKUP, BOOL, "0"}},
|
|
||||||
{"ToyotaEnhancedBsm", {PERSISTENT | BACKUP, BOOL, "0"}},
|
|
||||||
{"ToyotaTSS2Long", {PERSISTENT | BACKUP, BOOL, "0"}},
|
|
||||||
{"ToyotaDriveMode", {PERSISTENT | BACKUP, BOOL, "0"}},
|
|
||||||
|
|
||||||
// MADS params
|
// MADS params
|
||||||
{"Mads", {PERSISTENT | BACKUP, BOOL, "1"}},
|
{"Mads", {PERSISTENT | BACKUP, BOOL, "1"}},
|
||||||
{"MadsMainCruiseAllowed", {PERSISTENT | BACKUP, BOOL, "1"}},
|
{"MadsMainCruiseAllowed", {PERSISTENT | BACKUP, BOOL, "1"}},
|
||||||
@@ -240,10 +234,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"DynamicExperimentalControl", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"DynamicExperimentalControl", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"BlindSpot", {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
|
// sunnypilot model params
|
||||||
{"CameraOffset", {PERSISTENT | BACKUP, FLOAT, "0.0"}},
|
{"CameraOffset", {PERSISTENT | BACKUP, FLOAT, "0.0"}},
|
||||||
{"LagdToggle", {PERSISTENT | BACKUP, BOOL, "1"}},
|
{"LagdToggle", {PERSISTENT | BACKUP, BOOL, "1"}},
|
||||||
|
|||||||
@@ -117,16 +117,12 @@ class TestParams(OpenpilotTestCase):
|
|||||||
def test_params_default_value(self):
|
def test_params_default_value(self):
|
||||||
self.params.remove("LanguageSetting")
|
self.params.remove("LanguageSetting")
|
||||||
self.params.remove("LongitudinalPersonality")
|
self.params.remove("LongitudinalPersonality")
|
||||||
self.params.remove("AccelPersonalityEnabled")
|
|
||||||
self.params.remove("AccelPersonality")
|
|
||||||
self.params.remove("LiveParametersV2")
|
self.params.remove("LiveParametersV2")
|
||||||
|
|
||||||
assert self.params.get("LanguageSetting") is None
|
assert self.params.get("LanguageSetting") is None
|
||||||
assert self.params.get("LanguageSetting", return_default=False) 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("LanguageSetting", return_default=True), str)
|
||||||
assert isinstance(self.params.get("LongitudinalPersonality", return_default=True), int)
|
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") is None
|
||||||
assert self.params.get("LiveParametersV2", return_default=True) 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.params import Params
|
||||||
from openpilot.common.realtime import config_realtime_process, Priority, Ratekeeper
|
from openpilot.common.realtime import config_realtime_process, Priority, Ratekeeper
|
||||||
from openpilot.common.swaglog import cloudlog, ForwardingHandler
|
from openpilot.common.swaglog import cloudlog, ForwardingHandler
|
||||||
|
|
||||||
from opendbc.car import DT_CTRL, structs
|
from opendbc.car import DT_CTRL, structs
|
||||||
from opendbc.car.can_definitions import CanData, CanRecvCallable, CanSendCallable
|
from opendbc.car.can_definitions import CanData, CanRecvCallable, CanSendCallable
|
||||||
from opendbc.car.carlog import carlog
|
from opendbc.car.carlog import carlog
|
||||||
from opendbc.car.fw_versions import ObdCallback
|
from opendbc.car.fw_versions import ObdCallback
|
||||||
from opendbc.car.car_helpers import get_car, interfaces
|
from opendbc.car.car_helpers import get_car, interfaces
|
||||||
from opendbc.car.interfaces import CarInterfaceBase, RadarInterfaceBase
|
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.pandad import can_capnp_to_list, can_list_to_can_capnp
|
||||||
from openpilot.selfdrive.car.cruise import VCruiseHelper
|
from openpilot.selfdrive.car.cruise import VCruiseHelper
|
||||||
from openpilot.selfdrive.car.helpers import convert_carControlSP, convert_to_capnp
|
from openpilot.selfdrive.car.helpers import convert_carControlSP, convert_to_capnp
|
||||||
@@ -123,9 +123,6 @@ class Car:
|
|||||||
self.RI = RI
|
self.RI = RI
|
||||||
|
|
||||||
self.CP.alternativeExperience = 0
|
self.CP.alternativeExperience = 0
|
||||||
if self.params.get_bool("ToyotaAutoHold"):
|
|
||||||
self.CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.ALLOW_AEB
|
|
||||||
|
|
||||||
# mads
|
# mads
|
||||||
set_alternative_experience(self.CP, self.CP_SP, self.params)
|
set_alternative_experience(self.CP, self.CP_SP, self.params)
|
||||||
set_car_specific_params(self.CP, self.CP_SP, self.params)
|
set_car_specific_params(self.CP, self.CP_SP, self.params)
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ IMPERIAL_INCREMENT = round(CV.MPH_TO_KPH, 1) # round here to avoid rounding err
|
|||||||
ButtonEvent = car.CarState.ButtonEvent
|
ButtonEvent = car.CarState.ButtonEvent
|
||||||
ButtonType = car.CarState.ButtonEvent.Type
|
ButtonType = car.CarState.ButtonEvent.Type
|
||||||
CRUISE_LONG_PRESS = 50
|
CRUISE_LONG_PRESS = 50
|
||||||
TOYOTA_VIRTUAL_CRUISE_LONG_PRESS = 65
|
|
||||||
CRUISE_NEAREST_FUNC = {
|
CRUISE_NEAREST_FUNC = {
|
||||||
ButtonType.accelCruise: math.ceil,
|
ButtonType.accelCruise: math.ceil,
|
||||||
ButtonType.decelCruise: math.floor,
|
ButtonType.decelCruise: math.floor,
|
||||||
@@ -44,30 +43,6 @@ class VCruiseHelper(VCruiseHelperSP):
|
|||||||
def v_cruise_initialized(self):
|
def v_cruise_initialized(self):
|
||||||
return self.v_cruise_kph != V_CRUISE_UNSET
|
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):
|
def update_v_cruise(self, CS, enabled, is_metric):
|
||||||
self.v_cruise_kph_last = self.v_cruise_kph
|
self.v_cruise_kph_last = self.v_cruise_kph
|
||||||
|
|
||||||
@@ -76,21 +51,11 @@ class VCruiseHelper(VCruiseHelperSP):
|
|||||||
_enabled = self.update_enabled_state(CS, enabled)
|
_enabled = self.update_enabled_state(CS, enabled)
|
||||||
|
|
||||||
if CS.cruiseState.available:
|
if CS.cruiseState.available:
|
||||||
software_pcm_enabled = not self.CP_SP.pcmCruiseSpeed and _enabled
|
if not self.CP.pcmCruise or (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
|
# 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)
|
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.update_speed_limit_assist_v_cruise_non_pcm()
|
||||||
if self.software_pcm_cruise_speed:
|
self.v_cruise_cluster_kph = self.v_cruise_kph
|
||||||
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:
|
else:
|
||||||
self.v_cruise_kph = CS.cruiseState.speed * CV.MS_TO_KPH
|
self.v_cruise_kph = CS.cruiseState.speed * CV.MS_TO_KPH
|
||||||
self.v_cruise_cluster_kph = CS.cruiseState.speedCluster * CV.MS_TO_KPH
|
self.v_cruise_cluster_kph = CS.cruiseState.speedCluster * CV.MS_TO_KPH
|
||||||
@@ -120,13 +85,13 @@ class VCruiseHelper(VCruiseHelperSP):
|
|||||||
|
|
||||||
for b in CS.buttonEvents:
|
for b in CS.buttonEvents:
|
||||||
if b.type.raw in self.button_timers and not b.pressed:
|
if b.type.raw in self.button_timers and not b.pressed:
|
||||||
if self.button_timers[b.type.raw] > self.cruise_long_press_frames:
|
if self.button_timers[b.type.raw] > CRUISE_LONG_PRESS:
|
||||||
return # end long press
|
return # end long press
|
||||||
button_type = b.type.raw
|
button_type = b.type.raw
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
for k, timer in self.button_timers.items():
|
for k, timer in self.button_timers.items():
|
||||||
if timer and timer % self.cruise_long_press_frames == 0:
|
if timer and timer % CRUISE_LONG_PRESS == 0:
|
||||||
button_type = k
|
button_type = k
|
||||||
long_press = True
|
long_press = True
|
||||||
break
|
break
|
||||||
@@ -150,26 +115,10 @@ class VCruiseHelper(VCruiseHelperSP):
|
|||||||
return
|
return
|
||||||
|
|
||||||
long_press, v_cruise_delta = VCruiseHelperSP.update_v_cruise_delta(self, long_press, v_cruise_delta)
|
long_press, v_cruise_delta = VCruiseHelperSP.update_v_cruise_delta(self, long_press, v_cruise_delta)
|
||||||
# Toyota's canonical PCM set speed and displayed cluster set speed can differ. In
|
if long_press and self.v_cruise_kph % v_cruise_delta != 0: # partial interval
|
||||||
# software-owned PCM mode, round the value the driver sees and apply the same delta
|
self.v_cruise_kph = CRUISE_NEAREST_FUNC[button_type](self.v_cruise_kph / v_cruise_delta) * v_cruise_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:
|
else:
|
||||||
v_cruise_reference_new = v_cruise_reference + v_cruise_delta * CRUISE_INTERVAL_SIGN[button_type]
|
self.v_cruise_kph += 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 set is pressed while overriding, clip cruise speed to minimum of vEgo
|
||||||
if CS.gasPressed and button_type in (ButtonType.decelCruise, ButtonType.setCruise):
|
if CS.gasPressed and button_type in (ButtonType.decelCruise, ButtonType.setCruise):
|
||||||
@@ -178,12 +127,6 @@ class VCruiseHelper(VCruiseHelperSP):
|
|||||||
self.v_cruise_kph = np.clip(round(self.v_cruise_kph, 1), self.v_cruise_min, V_CRUISE_MAX)
|
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):
|
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
|
# increment timer for buttons still pressed
|
||||||
for k in self.button_timers:
|
for k in self.button_timers:
|
||||||
if self.button_timers[k] > 0:
|
if self.button_timers[k] > 0:
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ from openpilot.common.realtime import DT_CTRL
|
|||||||
from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N
|
from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N
|
||||||
from openpilot.common.pid import PIDController
|
from openpilot.common.pid import PIDController
|
||||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
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]
|
CONTROL_N_T_IDX = ModelConstants.T_IDXS[:CONTROL_N]
|
||||||
|
|
||||||
@@ -40,9 +39,8 @@ def long_control_state_trans(CP_SP, active, long_control_state,
|
|||||||
|
|
||||||
return long_control_state
|
return long_control_state
|
||||||
|
|
||||||
class LongControl(LongControlSP):
|
class LongControl:
|
||||||
def __init__(self, CP, CP_SP):
|
def __init__(self, CP, CP_SP):
|
||||||
LongControlSP.__init__(self)
|
|
||||||
self.CP = CP
|
self.CP = CP
|
||||||
self.CP_SP = CP_SP
|
self.CP_SP = CP_SP
|
||||||
self.long_control_state = LongCtrlState.off
|
self.long_control_state = LongCtrlState.off
|
||||||
@@ -61,17 +59,16 @@ class LongControl(LongControlSP):
|
|||||||
self.long_control_state = long_control_state_trans(self.CP_SP, active, self.long_control_state,
|
self.long_control_state = long_control_state_trans(self.CP_SP, active, self.long_control_state,
|
||||||
should_stop, CS.brakePressed,
|
should_stop, CS.brakePressed,
|
||||||
CS.cruiseState.standstill)
|
CS.cruiseState.standstill)
|
||||||
LongControlSP.update_state(self, self.long_control_state == LongCtrlState.stopping, active, CS)
|
|
||||||
if self.long_control_state == LongCtrlState.off:
|
if self.long_control_state == LongCtrlState.off:
|
||||||
self.reset()
|
self.reset()
|
||||||
output_accel = 0.
|
output_accel = 0.
|
||||||
|
|
||||||
elif self.long_control_state == LongCtrlState.stopping:
|
elif self.long_control_state == LongCtrlState.stopping:
|
||||||
output_accel = LongControlSP.stopping_accel(self, self.last_output_accel, CS)
|
output_accel = self.last_output_accel
|
||||||
if output_accel > self.CP.stopAccel:
|
if output_accel > self.CP.stopAccel:
|
||||||
output_accel = min(output_accel, 0.0)
|
output_accel = min(output_accel, 0.0)
|
||||||
# TODO: can we just go straight to stopAccel?
|
# TODO: can we just go straight to stopAccel?
|
||||||
output_accel -= LongControlSP.stopping_decel_rate(self, CS, a_target, output_accel) * DT_CTRL
|
output_accel -= 1.0 * DT_CTRL # m/s^2/s while trying to stop
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
else: # LongCtrlState.pid
|
else: # LongCtrlState.pid
|
||||||
|
|||||||
@@ -35,13 +35,8 @@ def get_max_accel(v_ego):
|
|||||||
def get_coast_accel(pitch):
|
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
|
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,
|
def get_cruise_accel(e2e, v_cruise, v_ego, a_cruise_prev, angle_steers, CP, dt, accel_coast, allow_throttle):
|
||||||
max_accel_override=None, min_accel_override=None):
|
max_accel = ACCEL_MAX if e2e else get_max_accel(v_ego)
|
||||||
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)
|
|
||||||
min_accel = A_CRUISE_MIN if e2e or min_accel_override is None else min_accel_override
|
|
||||||
|
|
||||||
if not e2e:
|
if not e2e:
|
||||||
a_total_max = np.interp(v_ego, _A_TOTAL_MAX_BP, _A_TOTAL_MAX_V)
|
a_total_max = np.interp(v_ego, _A_TOTAL_MAX_BP, _A_TOTAL_MAX_V)
|
||||||
@@ -53,18 +48,11 @@ def get_cruise_accel(e2e, v_cruise, v_ego, a_cruise_prev, angle_steers, CP, dt,
|
|||||||
coast_limit = np.interp(v_ego, [MIN_ALLOW_THROTTLE_SPEED, MIN_ALLOW_THROTTLE_SPEED*2], [max_accel, clipped_accel_coast])
|
coast_limit = np.interp(v_ego, [MIN_ALLOW_THROTTLE_SPEED, MIN_ALLOW_THROTTLE_SPEED*2], [max_accel, clipped_accel_coast])
|
||||||
max_accel = min(max_accel, coast_limit)
|
max_accel = min(max_accel, coast_limit)
|
||||||
|
|
||||||
target_accel = np.clip(v_cruise - v_ego, min_accel, max_accel)
|
target_accel = np.clip(v_cruise - v_ego, A_CRUISE_MIN, max_accel)
|
||||||
|
|
||||||
# An override only counts as "active" if it's the bound that actually determined target_accel here --
|
|
||||||
# turn/coast derating can shrink max_accel back below max_accel_override, and either bound can simply
|
|
||||||
# not be reached if v_cruise - v_ego already sits inside [min_accel, max_accel] on its own.
|
|
||||||
accel_controller_active = bool((max_accel_override is not None and max_accel == max_accel_override and target_accel == max_accel) or
|
|
||||||
(min_accel_override is not None and min_accel == min_accel_override and target_accel == min_accel))
|
|
||||||
|
|
||||||
j_cruise = np.interp(v_ego, A_CRUISE_MAX_BP, J_CRUISE_VALS)
|
j_cruise = np.interp(v_ego, A_CRUISE_MAX_BP, J_CRUISE_VALS)
|
||||||
target_accel = float(np.clip(target_accel, a_cruise_prev - j_cruise * dt, a_cruise_prev + j_cruise * dt))
|
target_accel = float(np.clip(target_accel, a_cruise_prev - j_cruise * dt, a_cruise_prev + j_cruise * dt))
|
||||||
|
|
||||||
return target_accel, accel_controller_active
|
return target_accel
|
||||||
|
|
||||||
|
|
||||||
class LongitudinalPlanner(LongitudinalPlannerSP):
|
class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||||
@@ -80,7 +68,6 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
|||||||
self.a_cruise = init_a
|
self.a_cruise = init_a
|
||||||
self.output_a_target = init_a
|
self.output_a_target = init_a
|
||||||
self.output_should_stop = False
|
self.output_should_stop = False
|
||||||
self.accel_controller_active = False
|
|
||||||
|
|
||||||
self.v_desired_trajectory = np.zeros(CONTROL_N)
|
self.v_desired_trajectory = np.zeros(CONTROL_N)
|
||||||
self.a_desired_trajectory = np.zeros(CONTROL_N)
|
self.a_desired_trajectory = np.zeros(CONTROL_N)
|
||||||
@@ -97,8 +84,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
|||||||
v_ego = sm['carState'].vEgo
|
v_ego = sm['carState'].vEgo
|
||||||
v_cruise_kph = min(sm['carState'].vCruise, V_CRUISE_MAX)
|
v_cruise_kph = min(sm['carState'].vCruise, V_CRUISE_MAX)
|
||||||
v_cruise = v_cruise_kph * CV.KPH_TO_MS
|
v_cruise = v_cruise_kph * CV.KPH_TO_MS
|
||||||
force_decel = sm['controlsState'].forceDecel
|
if sm['controlsState'].forceDecel:
|
||||||
if force_decel:
|
|
||||||
v_cruise = 0.0
|
v_cruise = 0.0
|
||||||
|
|
||||||
long_control_off = sm['controlsState'].longControlState == LongCtrlState.off
|
long_control_off = sm['controlsState'].longControlState == LongCtrlState.off
|
||||||
@@ -111,7 +97,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
|||||||
|
|
||||||
throttle_probs = sm['modelV2'].meta.disengagePredictions.gasPressProbs
|
throttle_probs = sm['modelV2'].meta.disengagePredictions.gasPressProbs
|
||||||
throttle_prob = throttle_probs[1] if len(throttle_probs) > 1 else 1.0
|
throttle_prob = throttle_probs[1] if len(throttle_probs) > 1 else 1.0
|
||||||
self.allow_throttle = self.update_allow_throttle(throttle_prob, low_speed_override=v_ego <= MIN_ALLOW_THROTTLE_SPEED, threshold=ALLOW_THROTTLE_THRESHOLD)
|
self.allow_throttle = throttle_prob > ALLOW_THROTTLE_THRESHOLD or v_ego <= MIN_ALLOW_THROTTLE_SPEED
|
||||||
|
|
||||||
steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['vehicleParameters'].angleOffsetDeg
|
steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['vehicleParameters'].angleOffsetDeg
|
||||||
|
|
||||||
@@ -132,7 +118,6 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
|||||||
self.mpc.set_weights(prev_accel_constraint, personality=sm['selfdriveState'].personality)
|
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.set_cur_state(self.v_desired_filter.x, self.output_a_target)
|
||||||
self.mpc.update(sm['radarState'], personality=sm['selfdriveState'].personality)
|
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.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)
|
self.a_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.a_solution)
|
||||||
@@ -150,17 +135,14 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
|||||||
output_a_target_mpc = get_accel_from_plan(self.v_desired_trajectory, self.a_desired_trajectory, CONTROL_N_T_IDX,
|
output_a_target_mpc = get_accel_from_plan(self.v_desired_trajectory, self.a_desired_trajectory, CONTROL_N_T_IDX,
|
||||||
action_t=action_t)
|
action_t=action_t)
|
||||||
output_should_stop_mpc = should_stop(v_ego, output_a_target_mpc)
|
output_should_stop_mpc = should_stop(v_ego, output_a_target_mpc)
|
||||||
output_should_stop_mpc = self.update_lead_departure(sm, output_a_target_mpc, output_should_stop_mpc, reset_state)
|
|
||||||
output_a_target_e2e = sm['modelV2'].action.desiredAcceleration
|
output_a_target_e2e = sm['modelV2'].action.desiredAcceleration
|
||||||
output_should_stop_e2e = sm['modelV2'].action.shouldStop
|
output_should_stop_e2e = sm['modelV2'].action.shouldStop
|
||||||
|
|
||||||
is_e2e = self.is_e2e(sm)
|
is_e2e = self.is_e2e(sm)
|
||||||
|
|
||||||
max_accel_override = self.get_max_accel_override(v_ego)
|
self.a_cruise = get_cruise_accel(is_e2e, v_cruise, v_ego,
|
||||||
min_accel_override = self.get_min_accel_override(v_ego, is_e2e, force_decel)
|
|
||||||
self.a_cruise, self.accel_controller_active = get_cruise_accel(is_e2e, v_cruise, v_ego,
|
|
||||||
self.a_cruise, steer_angle_without_offset, self.CP, self.dt,
|
self.a_cruise, steer_angle_without_offset, self.CP, self.dt,
|
||||||
accel_coast, self.allow_throttle, max_accel_override, min_accel_override)
|
accel_coast, self.allow_throttle)
|
||||||
cruise_should_stop = should_stop(v_ego, self.a_cruise)
|
cruise_should_stop = should_stop(v_ego, self.a_cruise)
|
||||||
|
|
||||||
candidates = [(output_a_target_mpc, self.mpc.source, output_should_stop_mpc),
|
candidates = [(output_a_target_mpc, self.mpc.source, output_should_stop_mpc),
|
||||||
|
|||||||
@@ -29,6 +29,12 @@ enum SpiError {
|
|||||||
|
|
||||||
const unsigned int SPI_ACK_TIMEOUT = 500; // milliseconds
|
const unsigned int SPI_ACK_TIMEOUT = 500; // milliseconds
|
||||||
const std::string SPI_DEVICE = "/dev/spidev0.0";
|
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 {
|
class LockEx {
|
||||||
public:
|
public:
|
||||||
@@ -319,6 +325,8 @@ int PandaSpiHandle::spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx
|
|||||||
assert(tx_len < SPI_BUF_SIZE);
|
assert(tx_len < SPI_BUF_SIZE);
|
||||||
assert(max_rx_len < SPI_BUF_SIZE);
|
assert(max_rx_len < SPI_BUF_SIZE);
|
||||||
|
|
||||||
|
wait_for_spi_turnaround(spi_last_bus_activity_ns);
|
||||||
|
|
||||||
xfer_count++;
|
xfer_count++;
|
||||||
header = {
|
header = {
|
||||||
.sync = SPI_SYNC,
|
.sync = SPI_SYNC,
|
||||||
@@ -347,6 +355,7 @@ int PandaSpiHandle::spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx
|
|||||||
if (ret < 0) {
|
if (ret < 0) {
|
||||||
goto fail;
|
goto fail;
|
||||||
}
|
}
|
||||||
|
wait_for_spi_turnaround(nanos_since_boot());
|
||||||
|
|
||||||
// Send data
|
// Send data
|
||||||
if (tx_data != NULL) {
|
if (tx_data != NULL) {
|
||||||
@@ -389,6 +398,7 @@ int PandaSpiHandle::spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx
|
|||||||
memcpy(rx_data, rx_buf + 3, rx_data_len);
|
memcpy(rx_data, rx_buf + 3, rx_data_len);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
spi_last_bus_activity_ns = nanos_since_boot();
|
||||||
return rx_data_len;
|
return rx_data_len;
|
||||||
|
|
||||||
fail:
|
fail:
|
||||||
@@ -403,6 +413,7 @@ fail:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
spi_last_bus_activity_ns = nanos_since_boot();
|
||||||
if (ret >= 0) ret = -1;
|
if (ret >= 0) ret = -1;
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,15 +11,6 @@ from openpilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPl
|
|||||||
from openpilot.selfdrive.controls.radard import _LEAD_ACCEL_TAU
|
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:
|
class Plant:
|
||||||
messaging_initialized = False
|
messaging_initialized = False
|
||||||
|
|
||||||
@@ -141,7 +132,7 @@ class Plant:
|
|||||||
car_control.carControl.orientationNED = [0., float(pitch), 0.]
|
car_control.carControl.orientationNED = [0., float(pitch), 0.]
|
||||||
|
|
||||||
# ******** get controlsState messages for plotting ***
|
# ******** get controlsState messages for plotting ***
|
||||||
sm = PlannerSM(self.rk.frame, {'radarState': radar.radarState,
|
sm = {'radarState': radar.radarState,
|
||||||
'carState': car_state.carState,
|
'carState': car_state.carState,
|
||||||
'carControl': car_control.carControl,
|
'carControl': car_control.carControl,
|
||||||
'controlsState': control.controlsState,
|
'controlsState': control.controlsState,
|
||||||
@@ -150,7 +141,7 @@ class Plant:
|
|||||||
'modelV2': model.modelV2,
|
'modelV2': model.modelV2,
|
||||||
'carStateSP': car_state_sp.carStateSP,
|
'carStateSP': car_state_sp.carStateSP,
|
||||||
'liveMapDataSP': live_map_data_sp.liveMapDataSP,
|
'liveMapDataSP': live_map_data_sp.liveMapDataSP,
|
||||||
'gpsLocation': gps_data.gpsLocation})
|
'gpsLocation': gps_data.gpsLocation}
|
||||||
self.planner.update(sm)
|
self.planner.update(sm)
|
||||||
self.acceleration = self.planner.output_a_target
|
self.acceleration = self.planner.output_a_target
|
||||||
if self.planner.output_should_stop:
|
if self.planner.output_should_stop:
|
||||||
|
|||||||
@@ -27,13 +27,6 @@ DESCRIPTIONS = {
|
|||||||
"In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with " +
|
"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."
|
"your steering wheel distance button."
|
||||||
),
|
),
|
||||||
"AccelPersonalityEnabled": tr_noop(
|
|
||||||
"Sets your preferred acceleration and cruise-deceleration limits by profile. Lead following, braking, and stopping behavior remain " +
|
|
||||||
"independent of this setting."
|
|
||||||
),
|
|
||||||
"AccelPersonality": tr_noop(
|
|
||||||
"Select the vehicle acceleration response. Chauffeur braking and stopping behavior remain the same across profiles."
|
|
||||||
),
|
|
||||||
"IsLdwEnabled": tr_noop(
|
"IsLdwEnabled": tr_noop(
|
||||||
"Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line " +
|
"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)."
|
"without a turn signal activated while driving over 31 mph (50 km/h)."
|
||||||
@@ -113,24 +106,6 @@ class TogglesLayout(Widget):
|
|||||||
icon="speed_limit.png"
|
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._toggles = {}
|
||||||
self._locked_toggles = set()
|
self._locked_toggles = set()
|
||||||
for param, (title, desc, icon, needs_restart) in self._toggle_defs.items():
|
for param, (title, desc, icon, needs_restart) in self._toggle_defs.items():
|
||||||
@@ -160,11 +135,9 @@ class TogglesLayout(Widget):
|
|||||||
|
|
||||||
self._toggles[param] = toggle
|
self._toggles[param] = toggle
|
||||||
|
|
||||||
# insert longitudinal personality and Accel Controller settings after NDOG toggle
|
# insert longitudinal personality after NDOG toggle
|
||||||
if param == "DisengageOnAccelerator":
|
if param == "DisengageOnAccelerator":
|
||||||
self._toggles["LongitudinalPersonality"] = self._long_personality_setting
|
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._update_experimental_mode_icon()
|
||||||
self._scroller = Scroller(list(self._toggles.values()), line_separator=True, spacing=0)
|
self._scroller = Scroller(list(self._toggles.values()), line_separator=True, spacing=0)
|
||||||
@@ -185,7 +158,6 @@ class TogglesLayout(Widget):
|
|||||||
|
|
||||||
def _update_toggles(self):
|
def _update_toggles(self):
|
||||||
ui_state.update_params()
|
ui_state.update_params()
|
||||||
accel_controller_enabled = self._params.get_bool("AccelPersonalityEnabled")
|
|
||||||
|
|
||||||
e2e_description = tr(
|
e2e_description = tr(
|
||||||
"sunnypilot defaults to driving in chill mode. Experimental mode enables alpha-level features that aren't ready for chill mode. " +
|
"sunnypilot defaults to driving in chill mode. Experimental mode enables alpha-level features that aren't ready for chill mode. " +
|
||||||
@@ -204,15 +176,11 @@ class TogglesLayout(Widget):
|
|||||||
self._toggles["ExperimentalMode"].action_item.set_enabled(True)
|
self._toggles["ExperimentalMode"].action_item.set_enabled(True)
|
||||||
self._toggles["ExperimentalMode"].set_description(e2e_description)
|
self._toggles["ExperimentalMode"].set_description(e2e_description)
|
||||||
self._long_personality_setting.action_item.set_enabled(True)
|
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:
|
else:
|
||||||
# no long for now
|
# no long for now
|
||||||
self._toggles["ExperimentalMode"].action_item.set_enabled(False)
|
self._toggles["ExperimentalMode"].action_item.set_enabled(False)
|
||||||
self._toggles["ExperimentalMode"].action_item.set_state(False)
|
self._toggles["ExperimentalMode"].action_item.set_state(False)
|
||||||
self._long_personality_setting.action_item.set_enabled(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")
|
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.")
|
unavailable = tr("Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control.")
|
||||||
@@ -235,8 +203,6 @@ class TogglesLayout(Widget):
|
|||||||
# refresh toggles from params to mirror external changes
|
# refresh toggles from params to mirror external changes
|
||||||
for param in self._toggle_defs:
|
for param in self._toggle_defs:
|
||||||
self._toggles[param].action_item.set_state(self._params.get_bool(param))
|
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
|
# these toggles need restart, block while engaged
|
||||||
for toggle_def in self._toggle_defs:
|
for toggle_def in self._toggle_defs:
|
||||||
@@ -281,9 +247,3 @@ class TogglesLayout(Widget):
|
|||||||
|
|
||||||
def _set_longitudinal_personality(self, button_index: int):
|
def _set_longitudinal_personality(self, button_index: int):
|
||||||
self._params.put("LongitudinalPersonality", button_index, block=True)
|
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,7 +14,6 @@ from openpilot.system.ui.lib.application import gui_app
|
|||||||
if gui_app.sunnypilot_ui():
|
if gui_app.sunnypilot_ui():
|
||||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.settings import SettingsLayoutSP as SettingsLayout
|
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.home import MiciHomeLayoutSP as MiciHomeLayout
|
||||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onroad import OnroadViewContainerSP as AugmentedRoadView
|
|
||||||
|
|
||||||
ONROAD_DELAY = 2.5 # seconds
|
ONROAD_DELAY = 2.5 # seconds
|
||||||
|
|
||||||
@@ -73,9 +72,6 @@ class MiciMainLayout(Scroller):
|
|||||||
# For scroll_to
|
# For scroll_to
|
||||||
return self._body_onroad_layout if ui_state.is_body else self._car_onroad_layout
|
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):
|
def _setup_callbacks(self):
|
||||||
self._home_layout.set_callbacks(
|
self._home_layout.set_callbacks(
|
||||||
on_settings=lambda: gui_app.push_widget(self._settings_layout),
|
on_settings=lambda: gui_app.push_widget(self._settings_layout),
|
||||||
@@ -126,15 +122,13 @@ class MiciMainLayout(Scroller):
|
|||||||
|
|
||||||
# FIXME: these two pops can interrupt user interacting in the settings
|
# 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:
|
if self._onroad_time_delay is not None and rl.get_time() - self._onroad_time_delay >= ONROAD_DELAY:
|
||||||
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))
|
||||||
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
|
|
||||||
self._onroad_time_delay = None
|
self._onroad_time_delay = None
|
||||||
|
|
||||||
# When car leaves standstill, pop nav stack and scroll to onroad
|
# When car leaves standstill, pop nav stack and scroll to onroad
|
||||||
CS = ui_state.sm["carState"]
|
CS = ui_state.sm["carState"]
|
||||||
if not CS.standstill and self._prev_standstill:
|
if not CS.standstill and self._prev_standstill:
|
||||||
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))
|
||||||
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
|
|
||||||
self._prev_standstill = CS.standstill
|
self._prev_standstill = CS.standstill
|
||||||
|
|
||||||
def _on_interactive_timeout(self):
|
def _on_interactive_timeout(self):
|
||||||
|
|||||||
@@ -42,8 +42,6 @@ class TogglesLayoutMici(NavScroller):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self._personality_toggle = BigMultiParamToggle("driving personality", "LongitudinalPersonality", ["aggressive", "standard", "relaxed"])
|
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"),
|
self._experimental_btn = BigToggle("experimental mode", initial_state=ui_state.params.get_bool("ExperimentalMode"),
|
||||||
toggle_callback=self._on_experimental_mode)
|
toggle_callback=self._on_experimental_mode)
|
||||||
is_metric_toggle = BigParamControl("use metric units", "IsMetric")
|
is_metric_toggle = BigParamControl("use metric units", "IsMetric")
|
||||||
@@ -55,8 +53,6 @@ class TogglesLayoutMici(NavScroller):
|
|||||||
|
|
||||||
self._scroller.add_widgets([
|
self._scroller.add_widgets([
|
||||||
self._personality_toggle,
|
self._personality_toggle,
|
||||||
self._accel_controller_enabled,
|
|
||||||
self._accel_personality_toggle,
|
|
||||||
self._experimental_btn,
|
self._experimental_btn,
|
||||||
is_metric_toggle,
|
is_metric_toggle,
|
||||||
ldw_toggle,
|
ldw_toggle,
|
||||||
@@ -69,7 +65,6 @@ class TogglesLayoutMici(NavScroller):
|
|||||||
# Toggle lists
|
# Toggle lists
|
||||||
self._refresh_toggles = (
|
self._refresh_toggles = (
|
||||||
("ExperimentalMode", self._experimental_btn),
|
("ExperimentalMode", self._experimental_btn),
|
||||||
("AccelPersonalityEnabled", self._accel_controller_enabled),
|
|
||||||
("IsMetric", is_metric_toggle),
|
("IsMetric", is_metric_toggle),
|
||||||
("IsLdwEnabled", ldw_toggle),
|
("IsLdwEnabled", ldw_toggle),
|
||||||
("AlwaysOnDM", always_on_dm_toggle),
|
("AlwaysOnDM", always_on_dm_toggle),
|
||||||
@@ -109,23 +104,17 @@ class TogglesLayoutMici(NavScroller):
|
|||||||
if ui_state.has_longitudinal_control:
|
if ui_state.has_longitudinal_control:
|
||||||
self._experimental_btn.set_visible(True)
|
self._experimental_btn.set_visible(True)
|
||||||
self._personality_toggle.set_visible(True)
|
self._personality_toggle.set_visible(True)
|
||||||
self._accel_controller_enabled.set_visible(True)
|
|
||||||
self._accel_personality_toggle.set_visible(True)
|
|
||||||
else:
|
else:
|
||||||
# no long for now
|
# no long for now
|
||||||
self._experimental_btn.set_visible(False)
|
self._experimental_btn.set_visible(False)
|
||||||
self._experimental_btn.set_checked(False)
|
self._experimental_btn.set_checked(False)
|
||||||
self._personality_toggle.set_visible(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")
|
ui_state.params.remove("ExperimentalMode")
|
||||||
|
|
||||||
# Refresh toggles from params to mirror external changes
|
# Refresh toggles from params to mirror external changes
|
||||||
for key, item in self._refresh_toggles:
|
for key, item in self._refresh_toggles:
|
||||||
item.set_checked(ui_state.params.get_bool(key))
|
item.set_checked(ui_state.params.get_bool(key))
|
||||||
|
|
||||||
self._accel_personality_toggle.refresh()
|
|
||||||
|
|
||||||
def _on_experimental_mode(self, state: bool):
|
def _on_experimental_mode(self, state: bool):
|
||||||
if state and not ui_state.params.get_bool("ExperimentalModeConfirmed"):
|
if state and not ui_state.params.get_bool("ExperimentalModeConfirmed"):
|
||||||
# Don't show enabled state until confirm
|
# Don't show enabled state until confirm
|
||||||
|
|||||||
@@ -154,8 +154,8 @@ class ModelRenderer(Widget, ModelRendererSP):
|
|||||||
self._draw_lane_lines()
|
self._draw_lane_lines()
|
||||||
self._draw_path(sm)
|
self._draw_path(sm)
|
||||||
|
|
||||||
if render_lead_indicator and radar_state:
|
# if render_lead_indicator and radar_state:
|
||||||
self._draw_lead_indicator()
|
# self._draw_lead_indicator()
|
||||||
|
|
||||||
def _update_raw_points(self, model):
|
def _update_raw_points(self, model):
|
||||||
"""Update raw 3D points from model data"""
|
"""Update raw 3D points from model data"""
|
||||||
|
|||||||
@@ -385,18 +385,13 @@ class BigMultiParamToggle(BigMultiToggle):
|
|||||||
self._load_value()
|
self._load_value()
|
||||||
|
|
||||||
def _load_value(self):
|
def _load_value(self):
|
||||||
value = self._params.get(self._param, return_default=True)
|
self.set_value(self._options[self._params.get(self._param) or 0])
|
||||||
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):
|
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||||
super()._handle_mouse_release(mouse_pos)
|
super()._handle_mouse_release(mouse_pos)
|
||||||
new_idx = self._options.index(self.value)
|
new_idx = self._options.index(self.value)
|
||||||
self._params.put(self._param, new_idx)
|
self._params.put(self._param, new_idx)
|
||||||
|
|
||||||
def refresh(self):
|
|
||||||
self._load_value()
|
|
||||||
|
|
||||||
|
|
||||||
class BigParamControl(BigToggle):
|
class BigParamControl(BigToggle):
|
||||||
def __init__(self, text: str, param: str, toggle_callback: Callable | None = None):
|
def __init__(self, text: str, param: str, toggle_callback: Callable | None = None):
|
||||||
|
|||||||
@@ -143,8 +143,7 @@ class CruiseLayout(Widget):
|
|||||||
self.icbm_toggle.show_description(True)
|
self.icbm_toggle.show_description(True)
|
||||||
|
|
||||||
if has_long or has_icbm:
|
if has_long or has_icbm:
|
||||||
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(((has_long and not ui_state.CP.pcmCruise) or has_icbm) and ui_state.is_offroad())
|
||||||
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.dec_toggle.action_item.set_enabled(has_long)
|
||||||
self.scc_v_toggle.action_item.set_enabled(True)
|
self.scc_v_toggle.action_item.set_enabled(True)
|
||||||
self.scc_m_toggle.action_item.set_enabled(True)
|
self.scc_m_toggle.action_item.set_enabled(True)
|
||||||
@@ -170,7 +169,7 @@ class CruiseLayout(Widget):
|
|||||||
show_custom_acc_desc = True
|
show_custom_acc_desc = True
|
||||||
else:
|
else:
|
||||||
if has_long or has_icbm:
|
if has_long or has_icbm:
|
||||||
if has_long and ui_state.CP.pcmCruise and ui_state.CP_SP.pcmCruiseSpeed:
|
if has_long and ui_state.CP.pcmCruise:
|
||||||
new_custom_acc_desc = tr(ACC_PCMCRUISE_DISABLED_DESCRIPTION)
|
new_custom_acc_desc = tr(ACC_PCMCRUISE_DISABLED_DESCRIPTION)
|
||||||
show_custom_acc_desc = True
|
show_custom_acc_desc = True
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ DESCRIPTIONS = {
|
|||||||
'stop_and_go_hack': tr_noop(
|
'stop_and_go_hack': tr_noop(
|
||||||
'sunnypilot will allow some Toyota/Lexus cars to auto resume during stop and go traffic. ' +
|
'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.'
|
'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.'
|
||||||
),
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
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()
|
|
||||||
@@ -4,11 +4,13 @@ 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.
|
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.
|
See the LICENSE.md file in the root directory for more details.
|
||||||
"""
|
"""
|
||||||
|
from collections.abc import Callable
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
|
|
||||||
from openpilot.cereal import custom
|
from openpilot.cereal import custom
|
||||||
from openpilot.sunnypilot.models.default_model import DEFAULT_MODEL
|
from openpilot.sunnypilot.models.default_model import DEFAULT_MODEL
|
||||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
|
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
|
||||||
|
from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialog
|
||||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.models import ModelsLayout
|
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.models import ModelsLayout
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
||||||
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||||
@@ -17,6 +19,24 @@ from openpilot.system.ui.widgets import Widget
|
|||||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||||
from openpilot.system.ui.widgets.scroller import NavScroller
|
from openpilot.system.ui.widgets.scroller import NavScroller
|
||||||
|
|
||||||
|
def _build_folders() -> dict[str, list]:
|
||||||
|
manager = ui_state.sm["modelManagerSP"]
|
||||||
|
bundles = manager.availableBundles
|
||||||
|
folders = {}
|
||||||
|
for bundle in bundles:
|
||||||
|
folder = next((override.value for override in bundle.overrides if override.key == "folder"), "")
|
||||||
|
folders.setdefault(folder, []).append(bundle)
|
||||||
|
|
||||||
|
favs = ui_state.params.get("ModelManager_Favs")
|
||||||
|
favorites = set(favs.split(';')) if favs else set()
|
||||||
|
|
||||||
|
if favorites:
|
||||||
|
for fav_bundle in [bundle for bundle in bundles if bundle.ref in favorites]:
|
||||||
|
folders.setdefault("favorites", []).append(fav_bundle)
|
||||||
|
|
||||||
|
return folders
|
||||||
|
|
||||||
|
|
||||||
class CurrentModelInfo(Widget):
|
class CurrentModelInfo(Widget):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -46,6 +66,41 @@ class CurrentModelInfo(Widget):
|
|||||||
self.info_text.set_position(self._rect.x + 20, self._rect.y + 161 - 25)
|
self.info_text.set_position(self._rect.x + 20, self._rect.y + 161 - 25)
|
||||||
self.info_text.render()
|
self.info_text.render()
|
||||||
|
|
||||||
|
|
||||||
|
class FolderSelectionMici(NavScroller):
|
||||||
|
|
||||||
|
def __init__(self, folder_name: str | None = None,
|
||||||
|
select_default_callback: Callable | None = None,
|
||||||
|
select_folder_callback: Callable | None = None,
|
||||||
|
select_model_callback: Callable | None = None):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
folders = _build_folders()
|
||||||
|
|
||||||
|
btns = []
|
||||||
|
if folder_name is None:
|
||||||
|
assert select_default_callback is not None and select_folder_callback is not None
|
||||||
|
default_btn = BigButton(f"{DEFAULT_MODEL} (Default)".lower())
|
||||||
|
default_btn.set_click_callback(select_default_callback)
|
||||||
|
btns.append(default_btn)
|
||||||
|
|
||||||
|
for folder in sorted(folders.keys(), key=lambda f: max((bundle.index for bundle in folders[f]), default=-1), reverse=True):
|
||||||
|
btn = BigButton(folder.lower())
|
||||||
|
btn.set_click_callback(lambda f=folder: select_folder_callback(f))
|
||||||
|
if folder.lower() == "favorites":
|
||||||
|
btns.insert(0, btn)
|
||||||
|
else:
|
||||||
|
btns.append(btn)
|
||||||
|
else:
|
||||||
|
assert select_model_callback is not None
|
||||||
|
for bundle in sorted(folders.get(folder_name, []), key=lambda b: b.index, reverse=True):
|
||||||
|
btn = BigButton(bundle.displayName.lower())
|
||||||
|
btn.set_click_callback(lambda b=bundle: select_model_callback(b))
|
||||||
|
btns.append(btn)
|
||||||
|
|
||||||
|
self._scroller.add_widgets(btns)
|
||||||
|
|
||||||
|
|
||||||
class ModelsLayoutMici(NavScroller):
|
class ModelsLayoutMici(NavScroller):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -59,81 +114,47 @@ class ModelsLayoutMici(NavScroller):
|
|||||||
self.select_model_btn = BigButton(tr("select model"))
|
self.select_model_btn = BigButton(tr("select model"))
|
||||||
self.select_model_btn.set_click_callback(self._show_folders)
|
self.select_model_btn.set_click_callback(self._show_folders)
|
||||||
|
|
||||||
|
self.clear_cache_btn = BigButton(tr("clear cache"), "")
|
||||||
|
self.clear_cache_btn.set_click_callback(self._clear_cache)
|
||||||
|
|
||||||
self.cancel_download_btn = BigButton(tr("cancel download"))
|
self.cancel_download_btn = BigButton(tr("cancel download"))
|
||||||
self.cancel_download_btn.set_click_callback(lambda: ui_state.params.remove("ModelManager_DownloadIndex"))
|
self.cancel_download_btn.set_click_callback(lambda: ui_state.params.remove("ModelManager_DownloadIndex"))
|
||||||
|
|
||||||
self.main_items = [self.current_model_info, self.select_model_btn, self.cancel_download_btn]
|
self.main_items = [self.current_model_info, self.select_model_btn, self.clear_cache_btn, self.cancel_download_btn]
|
||||||
self._scroller.add_widgets(self.main_items)
|
self._scroller.add_widgets(self.main_items)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def model_manager(self):
|
def model_manager(self):
|
||||||
return ui_state.sm["modelManagerSP"]
|
return ui_state.sm["modelManagerSP"]
|
||||||
|
|
||||||
def _get_grouped_bundles(self, favorites = None):
|
|
||||||
bundles = self.model_manager.availableBundles
|
|
||||||
folders = {}
|
|
||||||
for bundle in bundles:
|
|
||||||
folder = next((override.value for override in bundle.overrides if override.key == "folder"), "")
|
|
||||||
folders.setdefault(folder, []).append(bundle)
|
|
||||||
|
|
||||||
if favorites:
|
|
||||||
for fav_bundle in [bundle for bundle in bundles if bundle.ref in favorites]:
|
|
||||||
folders.setdefault("favorites", []).append(fav_bundle)
|
|
||||||
|
|
||||||
return folders
|
|
||||||
|
|
||||||
def _push_selection_view(self, items):
|
|
||||||
scroller = NavScroller()
|
|
||||||
scroller._scroller.add_widgets(items)
|
|
||||||
gui_app.push_widget(scroller)
|
|
||||||
|
|
||||||
def _show_folders(self):
|
def _show_folders(self):
|
||||||
self.focused_widget = self.select_model_btn
|
self.focused_widget = self.select_model_btn
|
||||||
|
|
||||||
favs = ui_state.params.get("ModelManager_Favs")
|
def select_default():
|
||||||
favorites = set(favs.split(';')) if favs else set()
|
ui_state.params.remove("ModelManager_ActiveBundle")
|
||||||
|
gui_app.pop_widgets_to(self, instant=True)
|
||||||
|
self._scroller.scroll_panel.set_offset(0)
|
||||||
|
self._scroller.scroll_to(0)
|
||||||
|
|
||||||
folders = self._get_grouped_bundles(favorites)
|
def select_model(bundle):
|
||||||
folder_buttons = []
|
ui_state.params.put("ModelManager_DownloadIndex", bundle.index)
|
||||||
default_btn = BigButton(f"{DEFAULT_MODEL} (Default)".lower())
|
gui_app.pop_widgets_to(self, instant=True)
|
||||||
default_btn.set_click_callback(self._select_default)
|
self._scroller.scroll_panel.set_offset(0)
|
||||||
folder_buttons.append(default_btn)
|
self._scroller.scroll_to(0)
|
||||||
|
|
||||||
for folder in sorted(folders.keys(), key=lambda f: max((bundle.index for bundle in folders[f]), default=-1), reverse=True):
|
def select_folder(folder_name):
|
||||||
if folder.lower() in ["release models", "master models", "favorites"]:
|
gui_app.push_widget(FolderSelectionMici(folder_name, select_model_callback=select_model))
|
||||||
btn = BigButton(folder.lower())
|
|
||||||
btn.set_click_callback(lambda f=folder: self._select_folder(f))
|
|
||||||
if folder.lower() == "favorites":
|
|
||||||
folder_buttons.insert(0, btn)
|
|
||||||
else:
|
|
||||||
folder_buttons.append(btn)
|
|
||||||
self._push_selection_view(folder_buttons)
|
|
||||||
|
|
||||||
def _pop_to_main(self):
|
gui_app.push_widget(FolderSelectionMici(select_default_callback=select_default, select_folder_callback=select_folder))
|
||||||
gui_app.pop_widgets_to(self)
|
|
||||||
|
|
||||||
def _select_model(self, bundle):
|
def _clear_cache(self):
|
||||||
ui_state.params.put("ModelManager_DownloadIndex", bundle.index)
|
def confirm_callback():
|
||||||
self._pop_to_main()
|
ui_state.params.put_bool("ModelManager_ClearCache", True)
|
||||||
|
|
||||||
def _select_default(self):
|
lbl = tr("slide to clear cache")
|
||||||
ui_state.params.remove("ModelManager_ActiveBundle")
|
icon = gui_app.texture("icons_mici/settings/device/uninstall.png", 64, 64)
|
||||||
self._pop_to_main()
|
dlg = BigConfirmationDialog(lbl, icon, confirm_callback=confirm_callback, red=True)
|
||||||
|
gui_app.push_widget(dlg)
|
||||||
def _select_folder(self, folder_name):
|
|
||||||
favs = ui_state.params.get("ModelManager_Favs")
|
|
||||||
favorites = set(favs.split(';')) if favs else set()
|
|
||||||
|
|
||||||
folders = self._get_grouped_bundles(favorites)
|
|
||||||
bundles = sorted(folders.get(folder_name, []), key=lambda b: b.index, reverse=True)
|
|
||||||
|
|
||||||
btns = []
|
|
||||||
for bundle in bundles:
|
|
||||||
txt = bundle.displayName.lower()
|
|
||||||
btn = BigButton(txt)
|
|
||||||
btn.set_click_callback(lambda b=bundle: self._select_model(b))
|
|
||||||
btns.append(btn)
|
|
||||||
self._push_selection_view(btns)
|
|
||||||
|
|
||||||
def hide_event(self):
|
def hide_event(self):
|
||||||
super().hide_event()
|
super().hide_event()
|
||||||
@@ -145,6 +166,7 @@ class ModelsLayoutMici(NavScroller):
|
|||||||
super()._update_state()
|
super()._update_state()
|
||||||
|
|
||||||
self.select_model_btn.set_enabled(ui_state.is_offroad())
|
self.select_model_btn.set_enabled(ui_state.is_offroad())
|
||||||
|
self.clear_cache_btn.set_enabled(ui_state.is_offroad())
|
||||||
self.cancel_download_btn.set_visible(False)
|
self.cancel_download_btn.set_visible(False)
|
||||||
self.current_model_info.current_model_header._shimmer = False
|
self.current_model_info.current_model_header._shimmer = False
|
||||||
self.current_model_info.info_header._shimmer = False
|
self.current_model_info.info_header._shimmer = False
|
||||||
@@ -191,4 +213,3 @@ class ModelsLayoutMici(NavScroller):
|
|||||||
self.current_model_info.info_header.set_text(tr("progress") + self._download_progress)
|
self.current_model_info.info_header.set_text(tr("progress") + self._download_progress)
|
||||||
self.current_model_info.info_header._shimmer = True
|
self.current_model_info.info_header._shimmer = True
|
||||||
self.current_model_info.info_text.set_text(f"{progress/count:.2f}%")
|
self.current_model_info.info_text.set_text(f"{progress/count:.2f}%")
|
||||||
|
|
||||||
|
|||||||
@@ -1,64 +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.
|
|
||||||
"""
|
|
||||||
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)
|
|
||||||
@@ -1,403 +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 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")
|
|
||||||
@@ -1,29 +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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
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)
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,33 +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 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()
|
|
||||||
@@ -1,16 +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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
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,9 +10,6 @@ from openpilot.selfdrive.ui.layouts.main import MainLayout
|
|||||||
from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout
|
from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
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()
|
BIG_UI = gui_app.big_ui()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ from openpilot.common.params import Params
|
|||||||
|
|
||||||
|
|
||||||
def get_lat_delay(params: Params, stock_lat_delay: float) -> float:
|
def get_lat_delay(params: Params, stock_lat_delay: float) -> float:
|
||||||
if params.get_bool("LagdToggle"):
|
# live learning on: use what lagd publishes.
|
||||||
return float(params.get("LagdValueCache", return_default=True))
|
# off: use the fixed steerActuatorDelay + software delay sum that LagdToggle caches.
|
||||||
|
|
||||||
return stock_lat_delay
|
if params.get_bool("LagdToggle"):
|
||||||
|
return stock_lat_delay
|
||||||
|
|
||||||
|
return float(params.get("LagdValueCache", return_default=True))
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ class ModelCache:
|
|||||||
class ModelFetcher:
|
class ModelFetcher:
|
||||||
"""Handles fetching and caching of model data from remote source"""
|
"""Handles fetching and caching of model data from remote source"""
|
||||||
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v20.json"
|
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v20.json"
|
||||||
MODEL_URL_USBGPU = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_usbgpu_v21.json"
|
MODEL_URL_USBGPU = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_usbgpu_v20.json"
|
||||||
|
|
||||||
def __init__(self, params: Params):
|
def __init__(self, params: Params):
|
||||||
self.params = params
|
self.params = params
|
||||||
|
|||||||
+1
-1
@@ -115,7 +115,7 @@ class IntelligentCruiseButtonManagement:
|
|||||||
self.is_ready = ready and not button_pressed
|
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:
|
def run(self, CS: car.CarState, CC: car.CarControl, LP_SP: custom.LongitudinalPlanSP, is_metric: bool) -> None:
|
||||||
if self.CP_SP.pcmCruiseSpeed or not self.CP_SP.intelligentCruiseButtonManagementAvailable:
|
if self.CP_SP.pcmCruiseSpeed:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.is_metric = is_metric
|
self.is_metric = is_metric
|
||||||
|
|||||||
@@ -136,9 +136,6 @@ def initialize_params(params) -> list[dict[str, Any]]:
|
|||||||
keys.extend([
|
keys.extend([
|
||||||
"ToyotaEnforceStockLongitudinal",
|
"ToyotaEnforceStockLongitudinal",
|
||||||
"ToyotaStopAndGoHack",
|
"ToyotaStopAndGoHack",
|
||||||
"ToyotaTSS2Long",
|
|
||||||
"ToyotaEnhancedBsm",
|
|
||||||
"ToyotaAutoHold",
|
|
||||||
])
|
])
|
||||||
|
|
||||||
return [{k: params.get(k, return_default=True)} for k in keys]
|
return [{k: params.get(k, return_default=True)} for k in keys]
|
||||||
|
|||||||
@@ -1,26 +1,14 @@
|
|||||||
from opendbc.can.parser import CANParser
|
|
||||||
from opendbc.car import create_button_events
|
|
||||||
from opendbc.car.structs import car
|
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.constants import CV
|
||||||
from openpilot.common.parameterized import parameterized, parameterized_class
|
from openpilot.common.parameterized import parameterized, parameterized_class
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
from openpilot.common.test import OpenpilotTestCase
|
from openpilot.selfdrive.car.cruise import V_CRUISE_INITIAL
|
||||||
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.selfdrive.car.tests.test_cruise_speed import TestVCruiseHelper
|
||||||
from openpilot.sunnypilot.selfdrive.car.interfaces import initialize_params
|
|
||||||
|
|
||||||
ButtonEvent = car.CarState.ButtonEvent
|
ButtonEvent = car.CarState.ButtonEvent
|
||||||
ButtonType = car.CarState.ButtonEvent.Type
|
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
|
# TODO: test pcmCruise and pcmCruiseSpeed
|
||||||
@parameterized_class(('pcm_cruise', 'pcm_cruise_speed'), [(False, True)])
|
@parameterized_class(('pcm_cruise', 'pcm_cruise_speed'), [(False, True)])
|
||||||
class TestCustomAccIncrements(TestVCruiseHelper):
|
class TestCustomAccIncrements(TestVCruiseHelper):
|
||||||
@@ -126,8 +114,8 @@ class TestCustomAccIncrements(TestVCruiseHelper):
|
|||||||
def test_rounding_behavior(self):
|
def test_rounding_behavior(self):
|
||||||
"""Test rounding behavior for 5 and 10 increments"""
|
"""Test rounding behavior for 5 and 10 increments"""
|
||||||
test_cases = [
|
test_cases = [
|
||||||
(47, 5, 50), # 47 -> 50 (round up to next 5)
|
(47, 5, 50), # 47 -> 50 (round up to next 5)
|
||||||
(45, 5, 50), # 45 -> 50 (already at 5, increment by 5)
|
(45, 5, 50), # 45 -> 50 (already at 5, increment by 5)
|
||||||
(43, 10, 50), # 43 -> 50 (round up to next 10)
|
(43, 10, 50), # 43 -> 50 (round up to next 10)
|
||||||
(40, 10, 50), # 40 -> 50 (already at 10, increment by 10)
|
(40, 10, 50), # 40 -> 50 (already at 10, increment by 10)
|
||||||
]
|
]
|
||||||
@@ -158,302 +146,3 @@ class TestCustomAccIncrements(TestVCruiseHelper):
|
|||||||
initial_speed = self.v_cruise_helper.v_cruise_kph
|
initial_speed = self.v_cruise_helper.v_cruise_kph
|
||||||
self.press_button_long(ButtonType.accelCruise)
|
self.press_button_long(ButtonType.accelCruise)
|
||||||
assert self.v_cruise_helper.v_cruise_kph == initial_speed + 10 # Should fallback to 10
|
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
|
|
||||||
|
|||||||
@@ -1,106 +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 math
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from openpilot.cereal import custom
|
|
||||||
from openpilot.common.filter_simple import FirstOrderFilter
|
|
||||||
from openpilot.common.params import Params
|
|
||||||
from openpilot.common.realtime import DT_MDL
|
|
||||||
from openpilot.sunnypilot import get_sanitize_int_param
|
|
||||||
|
|
||||||
AccelProfile = custom.LongitudinalPlanSP.AccelController.Profile
|
|
||||||
|
|
||||||
MAX_ACCEL_PROFILES = {
|
|
||||||
AccelProfile.eco: [1.45, 1.40, 1.20, 0.85, 0.62, 0.36, 0.22, 0.085, 0.055, 0.045],
|
|
||||||
AccelProfile.normal: [2.00, 1.95, 1.80, 1.06, 0.81, 0.69, 0.42, 0.160, 0.10, 0.08],
|
|
||||||
AccelProfile.sport: [2.00, 1.99, 1.95, 1.45, 1.10, 0.82, 0.53, 0.240, 0.13, 0.09],
|
|
||||||
}
|
|
||||||
MAX_ACCEL_BREAKPOINTS = [0., 3., 5., 8., 12., 18., 24., 32., 42., 55.]
|
|
||||||
|
|
||||||
MIN_ACCEL_PROFILES = {
|
|
||||||
AccelProfile.eco: [-0.90, -0.95, -1.00, -1.10, -1.2],
|
|
||||||
AccelProfile.normal: [-1.00, -1.05, -1.10, -1.20, -1.3],
|
|
||||||
AccelProfile.sport: [-1.10, -1.15, -1.20, -1.30, -1.4],
|
|
||||||
}
|
|
||||||
MIN_ACCEL_BREAKPOINTS = [3., 4.5, 7., 9., 25.]
|
|
||||||
|
|
||||||
ACCEL_SMOOTH_ALPHA = 0.90
|
|
||||||
DECEL_SMOOTH_ALPHA = 0.40
|
|
||||||
ALLOW_THROTTLE_FILTER_RC = 0.10
|
|
||||||
ALLOW_THROTTLE_HYSTERESIS = 0.05
|
|
||||||
|
|
||||||
class AccelController:
|
|
||||||
def __init__(self, dt: float = DT_MDL):
|
|
||||||
self.params = Params()
|
|
||||||
self.frame = 0
|
|
||||||
self.last_max_accel = 2.0
|
|
||||||
self.last_min_accel = -0.01
|
|
||||||
self.first_run = True
|
|
||||||
self.min_accel_first_run = True
|
|
||||||
self._profile = get_sanitize_int_param("AccelPersonality", AccelProfile.eco, AccelProfile.sport, self.params)
|
|
||||||
self._enabled = self.params.get_bool("AccelPersonalityEnabled")
|
|
||||||
self._allow_throttle = True
|
|
||||||
self._throttle_prob_filter = FirstOrderFilter(0.0, ALLOW_THROTTLE_FILTER_RC, dt, initialized=False)
|
|
||||||
|
|
||||||
def update(self, sm=None) -> None:
|
|
||||||
self.frame += 1
|
|
||||||
if self.frame % int(1.0 / DT_MDL) == 0:
|
|
||||||
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 update_allow_throttle(self, throttle_prob: float, low_speed_override: bool, threshold: float) -> bool:
|
|
||||||
if low_speed_override:
|
|
||||||
self._allow_throttle = True
|
|
||||||
self._throttle_prob_filter.x = 0.0
|
|
||||||
self._throttle_prob_filter.initialized = False
|
|
||||||
return True
|
|
||||||
|
|
||||||
if not math.isfinite(throttle_prob):
|
|
||||||
self._allow_throttle = False
|
|
||||||
self._throttle_prob_filter.x = 0.0
|
|
||||||
self._throttle_prob_filter.initialized = True
|
|
||||||
return False
|
|
||||||
|
|
||||||
filtered_throttle_prob = self._throttle_prob_filter.update(throttle_prob)
|
|
||||||
allow_threshold = threshold if self._allow_throttle else threshold + ALLOW_THROTTLE_HYSTERESIS
|
|
||||||
self._allow_throttle = bool(filtered_throttle_prob > allow_threshold)
|
|
||||||
return self._allow_throttle
|
|
||||||
|
|
||||||
def get_max_accel(self, v_ego: float) -> float:
|
|
||||||
v_ego = max(0.0, v_ego)
|
|
||||||
target_max = np.interp(v_ego, MAX_ACCEL_BREAKPOINTS, MAX_ACCEL_PROFILES[self._profile])
|
|
||||||
|
|
||||||
if self.first_run:
|
|
||||||
self.last_max_accel = target_max
|
|
||||||
self.first_run = False
|
|
||||||
return float(target_max)
|
|
||||||
|
|
||||||
self.last_max_accel = ACCEL_SMOOTH_ALPHA * target_max + (1 - ACCEL_SMOOTH_ALPHA) * self.last_max_accel
|
|
||||||
return float(self.last_max_accel)
|
|
||||||
|
|
||||||
def get_min_accel(self, v_ego: float) -> float:
|
|
||||||
v_ego = max(0.0, v_ego)
|
|
||||||
target_min = np.interp(v_ego, MIN_ACCEL_BREAKPOINTS, MIN_ACCEL_PROFILES[self._profile])
|
|
||||||
|
|
||||||
if self.min_accel_first_run:
|
|
||||||
self.last_min_accel = target_min
|
|
||||||
self.min_accel_first_run = False
|
|
||||||
else:
|
|
||||||
self.last_min_accel = DECEL_SMOOTH_ALPHA * target_min + (1 - DECEL_SMOOTH_ALPHA) * self.last_min_accel
|
|
||||||
|
|
||||||
self.last_min_accel = min(self.last_min_accel, self.last_max_accel - 0.1)
|
|
||||||
return float(self.last_min_accel)
|
|
||||||
-227
@@ -1,227 +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.
|
|
||||||
|
|
||||||
Scope is deliberately narrow: a v_ego-keyed acceleration ceiling and cruise-deceleration
|
|
||||||
floor per profile. The controller does not modify lead following distance or the MPC lead
|
|
||||||
candidate. The floor only ever softens the no-lead cruise candidate (slowing for a lower
|
|
||||||
cruise speed, a curve, or a speed limit); it is excluded during forceDecel and e2e, and
|
|
||||||
min() against the untouched MPC candidate means a real lead can always still force full
|
|
||||||
ACCEL_MIN braking.
|
|
||||||
|
|
||||||
Ceiling vs floor apply on different policies: ACC (non-e2e) uses the controller's ceiling
|
|
||||||
and floor; blended (e2e) uses the controller's ceiling but always the stock floor
|
|
||||||
(A_CRUISE_MIN).
|
|
||||||
"""
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from openpilot.cereal import messaging
|
|
||||||
from openpilot.common.params import Params
|
|
||||||
from openpilot.common.realtime import DT_MDL
|
|
||||||
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, MIN_ACCEL_BREAKPOINTS, MIN_ACCEL_PROFILES,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestAccelControllerCeiling(OpenpilotTestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.params = Params()
|
|
||||||
self.params.put_bool("AccelPersonalityEnabled", True, block=True)
|
|
||||||
self.params.put("AccelPersonality", AccelProfile.normal, block=True)
|
|
||||||
self.controller = AccelController()
|
|
||||||
|
|
||||||
def test_first_call_snaps_to_table_with_no_smoothing_lag(self):
|
|
||||||
max_a = self.controller.get_max_accel(20.0)
|
|
||||||
expected_max = np.interp(20.0, MAX_ACCEL_BREAKPOINTS, MAX_ACCEL_PROFILES[AccelProfile.normal])
|
|
||||||
self.assertAlmostEqual(max_a, expected_max, places=6)
|
|
||||||
|
|
||||||
def test_min_accel_first_call_snaps_to_table_not_the_neg0p01_seed(self):
|
|
||||||
# Regression guard: get_min_accel used to have no first-run snap (unlike get_max_accel),
|
|
||||||
# so its very first call blended the table target against a hardcoded -0.01 seed and
|
|
||||||
# commanded a much-weaker-than-any-profile floor for the first ~10-15 frames of every drive.
|
|
||||||
min_a = self.controller.get_min_accel(20.0)
|
|
||||||
expected_min = np.interp(20.0, MIN_ACCEL_BREAKPOINTS, MIN_ACCEL_PROFILES[AccelProfile.normal])
|
|
||||||
self.assertAlmostEqual(min_a, expected_min, places=6)
|
|
||||||
|
|
||||||
def test_table_lookup_matches_breakpoints_per_profile(self):
|
|
||||||
for profile, table in MAX_ACCEL_PROFILES.items():
|
|
||||||
self.params.put("AccelPersonality", profile, block=True)
|
|
||||||
controller = AccelController()
|
|
||||||
for v_ego, expected in zip(MAX_ACCEL_BREAKPOINTS, table, strict=True):
|
|
||||||
controller.first_run = True
|
|
||||||
max_a = controller.get_max_accel(v_ego)
|
|
||||||
self.assertAlmostEqual(max_a, expected, places=3)
|
|
||||||
|
|
||||||
def test_smoothing_moves_gradually_not_instantly_on_profile_switch(self):
|
|
||||||
v_ego = 8.0 # breakpoint where eco/normal/sport ceilings differ
|
|
||||||
self.controller.get_max_accel(v_ego) # settle first_run on normal
|
|
||||||
start = self.controller.last_max_accel
|
|
||||||
self.params.put("AccelPersonality", AccelProfile.sport, block=True)
|
|
||||||
self.controller.frame = int(1.0 / DT_MDL) - 1 # force the 1s refresh boundary on next update()
|
|
||||||
self.controller.update()
|
|
||||||
max_a = self.controller.get_max_accel(v_ego)
|
|
||||||
target = MAX_ACCEL_PROFILES[AccelProfile.sport][MAX_ACCEL_BREAKPOINTS.index(v_ego)]
|
|
||||||
self.assertNotEqual(start, target)
|
|
||||||
self.assertGreater(max_a, start)
|
|
||||||
self.assertLess(max_a, target)
|
|
||||||
|
|
||||||
def test_eco_is_selectable_not_treated_as_falsy(self):
|
|
||||||
self.params.put("AccelPersonality", AccelProfile.eco, block=True)
|
|
||||||
controller = AccelController()
|
|
||||||
self.assertEqual(controller.profile, AccelProfile.eco)
|
|
||||||
max_a = controller.get_max_accel(0.0)
|
|
||||||
self.assertAlmostEqual(max_a, MAX_ACCEL_PROFILES[AccelProfile.eco][0], places=3)
|
|
||||||
|
|
||||||
def test_min_accel_never_stronger_than_stock_a_cruise_min(self):
|
|
||||||
for v_ego in [0., 3., 4.5, 7., 9., 15., 25., 40.]:
|
|
||||||
for _ in range(60):
|
|
||||||
min_a = self.controller.get_min_accel(v_ego)
|
|
||||||
self.assertGreaterEqual(min_a, -1.4) # softer or equal to the softest stock-adjacent floor, never harsher
|
|
||||||
self.assertLess(min_a, 0.0)
|
|
||||||
|
|
||||||
def test_min_accel_ramps_to_stock_strength_by_highway_speed(self):
|
|
||||||
for _ in range(200):
|
|
||||||
min_a = self.controller.get_min_accel(25.0)
|
|
||||||
self.assertAlmostEqual(min_a, MIN_ACCEL_PROFILES[AccelProfile.normal][-1], places=2)
|
|
||||||
|
|
||||||
def test_min_accel_profile_ordering_eco_softest_sport_strongest(self):
|
|
||||||
settled = {}
|
|
||||||
for profile in (AccelProfile.eco, AccelProfile.normal, AccelProfile.sport):
|
|
||||||
self.params.put("AccelPersonality", profile, block=True)
|
|
||||||
controller = AccelController()
|
|
||||||
for _ in range(60):
|
|
||||||
settled[profile] = controller.get_min_accel(4.5)
|
|
||||||
self.assertGreater(settled[AccelProfile.eco], settled[AccelProfile.normal])
|
|
||||||
self.assertGreater(settled[AccelProfile.normal], settled[AccelProfile.sport])
|
|
||||||
|
|
||||||
def test_min_accel_never_inverts_above_max_accel(self):
|
|
||||||
# Both feed the same np.clip call in get_cruise_accel -- independent smoothing must
|
|
||||||
# never let the floor drift above the ceiling.
|
|
||||||
for v_ego in [0., 3., 8., 20., 45.]:
|
|
||||||
max_a = self.controller.get_max_accel(v_ego)
|
|
||||||
min_a = self.controller.get_min_accel(v_ego)
|
|
||||||
self.assertLessEqual(min_a, max_a - 0.05)
|
|
||||||
|
|
||||||
def test_params_refresh_only_at_one_second_boundary(self):
|
|
||||||
self.controller.frame = 0
|
|
||||||
self.params.put("AccelPersonality", AccelProfile.sport, block=True)
|
|
||||||
self.controller.update() # frame=1, not a boundary
|
|
||||||
self.assertEqual(self.controller.profile, AccelProfile.normal)
|
|
||||||
self.controller.frame = int(1.0 / DT_MDL) - 1
|
|
||||||
self.controller.update() # crosses the boundary
|
|
||||||
self.assertEqual(self.controller.profile, AccelProfile.sport)
|
|
||||||
|
|
||||||
def test_enabled_reflects_params(self):
|
|
||||||
self.params.put_bool("AccelPersonalityEnabled", False, block=True)
|
|
||||||
controller = AccelController()
|
|
||||||
self.assertFalse(controller.is_enabled())
|
|
||||||
self.params.put_bool("AccelPersonalityEnabled", True, block=True)
|
|
||||||
controller.frame = int(1.0 / DT_MDL) - 1
|
|
||||||
controller.update()
|
|
||||||
self.assertTrue(controller.is_enabled())
|
|
||||||
|
|
||||||
def test_max_accel_never_exceeds_profile_ceiling(self):
|
|
||||||
for v_ego in [0., 5., 10., 20., 30., 45., 60.]:
|
|
||||||
max_a = self.controller.get_max_accel(v_ego)
|
|
||||||
table_max = max(max(table) for table in MAX_ACCEL_PROFILES.values())
|
|
||||||
self.assertLessEqual(max_a, table_max + 1e-6)
|
|
||||||
|
|
||||||
|
|
||||||
class TestOffEqualsStock(OpenpilotTestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.params = Params()
|
|
||||||
self.params.put_bool("AccelPersonalityEnabled", False, block=True)
|
|
||||||
|
|
||||||
def test_disabled_controller_is_enabled_returns_false(self):
|
|
||||||
controller = AccelController()
|
|
||||||
self.assertFalse(controller.is_enabled())
|
|
||||||
|
|
||||||
def test_get_cruise_accel_with_none_override_matches_no_kwarg(self):
|
|
||||||
from openpilot.selfdrive.controls.lib.longitudinal_planner import get_cruise_accel
|
|
||||||
args = (False, 10.0, 8.0, 0.5, 0.0, _fake_cp(), DT_MDL, 1.0, True)
|
|
||||||
self.assertEqual(get_cruise_accel(*args), get_cruise_accel(*args, max_accel_override=None, min_accel_override=None))
|
|
||||||
|
|
||||||
def test_disabled_min_accel_override_is_none(self):
|
|
||||||
planner = _bare_planner()
|
|
||||||
self.assertIsNone(planner.get_min_accel_override(v_ego=5.0, e2e=False, force_decel=False))
|
|
||||||
|
|
||||||
def test_disabled_max_accel_override_is_none(self):
|
|
||||||
planner = _bare_planner()
|
|
||||||
self.assertIsNone(planner.get_max_accel_override(v_ego=5.0))
|
|
||||||
|
|
||||||
def test_force_decel_excludes_min_accel_override_even_when_enabled(self):
|
|
||||||
self.params.put_bool("AccelPersonalityEnabled", True, block=True)
|
|
||||||
planner = _bare_planner()
|
|
||||||
self.assertIsNone(planner.get_min_accel_override(v_ego=5.0, e2e=False, force_decel=True))
|
|
||||||
|
|
||||||
def test_e2e_excludes_min_accel_override_even_when_enabled(self):
|
|
||||||
self.params.put_bool("AccelPersonalityEnabled", True, block=True)
|
|
||||||
planner = _bare_planner()
|
|
||||||
self.assertIsNone(planner.get_min_accel_override(v_ego=5.0, e2e=True, force_decel=False))
|
|
||||||
|
|
||||||
def test_enabled_min_accel_override_returns_a_float(self):
|
|
||||||
self.params.put_bool("AccelPersonalityEnabled", True, block=True)
|
|
||||||
planner = _bare_planner()
|
|
||||||
override = planner.get_min_accel_override(v_ego=5.0, e2e=False, force_decel=False)
|
|
||||||
self.assertIsNotNone(override)
|
|
||||||
self.assertLess(override, 0.0)
|
|
||||||
|
|
||||||
def test_enabled_max_accel_override_applies_in_acc_and_blended(self):
|
|
||||||
# Policy: max ceiling comes from AccelController in both ACC and blended (e2e) modes --
|
|
||||||
# only the min floor is blended-vs-stock. get_max_accel_override no longer takes an e2e
|
|
||||||
# arg because of this; the caller applies it unconditionally.
|
|
||||||
self.params.put_bool("AccelPersonalityEnabled", True, block=True)
|
|
||||||
planner = _bare_planner()
|
|
||||||
override = planner.get_max_accel_override(v_ego=5.0)
|
|
||||||
self.assertIsNotNone(override)
|
|
||||||
self.assertGreater(override, 0.0)
|
|
||||||
|
|
||||||
def test_blended_min_accel_uses_stock_not_controller(self):
|
|
||||||
# e2e/blended braking floor is deliberately left at stock's A_CRUISE_MIN, never the
|
|
||||||
# controller's floor -- this is the "acc policy = controller min+max, blended policy =
|
|
||||||
# controller max + stock min" split, final per product decision.
|
|
||||||
# jerk-limiting now applies unconditionally (even in e2e, per upstream's decel-jerk fix), so
|
|
||||||
# dt=10.0 opens the jerk-limit window wide enough that it can't mask the floor/ceiling asserted here.
|
|
||||||
from openpilot.selfdrive.controls.lib.longitudinal_planner import get_cruise_accel, A_CRUISE_MIN
|
|
||||||
args = {"v_cruise": -100.0, "v_ego": 20.0, "a_cruise_prev": 0.0, "angle_steers": 0.0, "CP": _fake_cp(),
|
|
||||||
"dt": 10.0, "accel_coast": 1.0, "allow_throttle": True}
|
|
||||||
target, active = get_cruise_accel(True, **args, min_accel_override=-0.3)
|
|
||||||
self.assertAlmostEqual(target, A_CRUISE_MIN, places=6)
|
|
||||||
self.assertFalse(active) # controller's floor was ignored in favor of stock -- not "active"
|
|
||||||
|
|
||||||
def test_blended_max_accel_uses_controller_override(self):
|
|
||||||
# jerk-limiting now applies unconditionally (even in e2e) -- dt=10.0 opens the jerk-limit
|
|
||||||
# window wide enough that it can't mask the override ceiling asserted here.
|
|
||||||
from openpilot.selfdrive.controls.lib.longitudinal_planner import get_cruise_accel
|
|
||||||
args = {"v_cruise": 100.0, "v_ego": 20.0, "a_cruise_prev": 0.0, "angle_steers": 0.0, "CP": _fake_cp(),
|
|
||||||
"dt": 10.0, "accel_coast": 1.0, "allow_throttle": True}
|
|
||||||
target, active = get_cruise_accel(True, **args, max_accel_override=0.4)
|
|
||||||
self.assertAlmostEqual(target, 0.4, places=6)
|
|
||||||
self.assertTrue(active)
|
|
||||||
self.assertIsInstance(active, bool)
|
|
||||||
plan = messaging.new_message('longitudinalPlanSP')
|
|
||||||
plan.longitudinalPlanSP.accelController.active = active
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _fake_cp():
|
|
||||||
class _CP:
|
|
||||||
steerRatio = 15.0
|
|
||||||
wheelbase = 2.7
|
|
||||||
return _CP()
|
|
||||||
|
|
||||||
|
|
||||||
def _bare_planner():
|
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlannerSP
|
|
||||||
planner = LongitudinalPlannerSP.__new__(LongitudinalPlannerSP)
|
|
||||||
planner.accel_controller = AccelController()
|
|
||||||
return planner
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
-94
@@ -1,94 +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 math
|
|
||||||
|
|
||||||
from openpilot.common.params import Params
|
|
||||||
from openpilot.common.test import OpenpilotTestCase
|
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.accel_controller.accel_controller import AccelController
|
|
||||||
|
|
||||||
|
|
||||||
class TestAllowThrottle(OpenpilotTestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.controller = AccelController()
|
|
||||||
|
|
||||||
def update(self, throttle_prob: float, low_speed_override: bool = False) -> bool:
|
|
||||||
return self.controller.update_allow_throttle(throttle_prob, low_speed_override=low_speed_override, threshold=0.4)
|
|
||||||
|
|
||||||
def test_short_probability_dip(self):
|
|
||||||
self.assertTrue(self.update(1.0))
|
|
||||||
self.assertTrue(self.update(0.0))
|
|
||||||
self.assertTrue(self.update(0.0))
|
|
||||||
self.assertFalse(self.update(0.0))
|
|
||||||
|
|
||||||
def test_probability_chatter(self):
|
|
||||||
self.assertTrue(self.update(1.0))
|
|
||||||
for _ in range(100):
|
|
||||||
self.assertTrue(self.update(0.39))
|
|
||||||
self.assertTrue(self.update(0.46))
|
|
||||||
|
|
||||||
self.controller = AccelController()
|
|
||||||
self.assertFalse(self.update(0.0))
|
|
||||||
for _ in range(100):
|
|
||||||
self.assertFalse(self.update(0.39))
|
|
||||||
self.assertFalse(self.update(0.46))
|
|
||||||
|
|
||||||
def test_sustained_probability_changes(self):
|
|
||||||
self.assertTrue(self.update(1.0))
|
|
||||||
self.assertEqual([self.update(0.0) for _ in range(4)], [True, True, False, False])
|
|
||||||
|
|
||||||
for _ in range(20):
|
|
||||||
self.assertFalse(self.update(0.0))
|
|
||||||
self.assertEqual([self.update(1.0) for _ in range(2)], [False, True])
|
|
||||||
|
|
||||||
def test_threshold_boundaries(self):
|
|
||||||
self.assertFalse(self.update(0.4))
|
|
||||||
|
|
||||||
self.controller._throttle_prob_filter.initialized = False
|
|
||||||
self.assertFalse(self.update(0.45))
|
|
||||||
|
|
||||||
self.controller._throttle_prob_filter.initialized = False
|
|
||||||
self.assertTrue(self.update(math.nextafter(0.45, math.inf)))
|
|
||||||
|
|
||||||
def test_low_speed_override(self):
|
|
||||||
for _ in range(20):
|
|
||||||
self.assertTrue(self.update(0.0, True))
|
|
||||||
|
|
||||||
self.assertTrue(self.update(1.0))
|
|
||||||
self.assertTrue(self.update(math.nan, True))
|
|
||||||
self.assertTrue(self.update(1.0))
|
|
||||||
self.assertTrue(self.update(0.0, True))
|
|
||||||
self.assertFalse(self.update(0.0))
|
|
||||||
|
|
||||||
def test_nonfinite_probability(self):
|
|
||||||
self.assertTrue(self.update(1.0))
|
|
||||||
for value in (math.inf, -math.inf, math.nan):
|
|
||||||
self.assertFalse(self.update(value))
|
|
||||||
self.assertTrue(math.isfinite(self.controller._throttle_prob_filter.x))
|
|
||||||
|
|
||||||
self.assertFalse(self.update(1.0))
|
|
||||||
self.assertTrue(self.update(1.0))
|
|
||||||
|
|
||||||
def test_route_probability_trace(self):
|
|
||||||
probabilities = (0.941, 0.093, 0.070, 0.429, 0.430, 0.083, 0.509, 0.068)
|
|
||||||
states = [self.update(probability) for probability in probabilities]
|
|
||||||
self.assertEqual(states, [True, True, True, True, True, False, False, False])
|
|
||||||
|
|
||||||
def test_filter_updates_once(self):
|
|
||||||
self.assertTrue(self.update(1.0))
|
|
||||||
self.assertTrue(self.update(0.0))
|
|
||||||
self.assertAlmostEqual(self.controller._throttle_prob_filter.x, 2.0 / 3.0)
|
|
||||||
|
|
||||||
def test_profiles_disabled(self):
|
|
||||||
Params().put_bool("AccelPersonalityEnabled", False, block=True)
|
|
||||||
self.controller = AccelController()
|
|
||||||
self.assertFalse(self.controller.is_enabled())
|
|
||||||
|
|
||||||
self.assertTrue(self.update(1.0))
|
|
||||||
self.assertTrue(self.update(0.0))
|
|
||||||
self.assertTrue(self.update(0.0))
|
|
||||||
self.assertFalse(self.update(0.0))
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
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,116 +4,192 @@ Copyright (c) 2021-, rav4kumar, sunnypilot, and a number of other contributors.
|
|||||||
This file is part of sunnypilot and is licensed under the MIT License.
|
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.
|
See the LICENSE.md file in the root directory for more details.
|
||||||
"""
|
"""
|
||||||
from dataclasses import dataclass
|
# Version = 2025-6-30
|
||||||
from typing import Literal
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from openpilot.cereal import messaging
|
from openpilot.cereal import messaging
|
||||||
from opendbc.car import structs
|
from opendbc.car import structs
|
||||||
|
from numpy import interp
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
from openpilot.common.realtime import DT_MDL
|
||||||
|
from openpilot.sunnypilot.selfdrive.controls.lib.dec.constants import WMACConstants
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
# d-e2e, from modeldata.h
|
||||||
|
TRAJECTORY_SIZE = 33
|
||||||
|
SET_MODE_TIMEOUT = 15
|
||||||
|
|
||||||
|
# Define the valid mode types
|
||||||
ModeType = Literal['acc', 'blended']
|
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]
|
|
||||||
|
|
||||||
DECEL_INTENT_A_HINT = 0.35
|
class SmoothKalmanFilter:
|
||||||
DECEL_INTENT_A_FULL = 1.30
|
"""Enhanced Kalman filter with smoothing for stable decision making."""
|
||||||
DECEL_INTENT_TRIGGER = 0.5
|
|
||||||
|
|
||||||
CURVE_Y_MAX = 5.0
|
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
|
||||||
|
|
||||||
LEAD_FUTURE_PROB_VANISH = 0.35
|
def add_data(self, measurement):
|
||||||
|
if len(self.history) >= self.max_history:
|
||||||
|
self.history.pop(0)
|
||||||
|
self.history.append(measurement)
|
||||||
|
|
||||||
MODEL_DROP_TRUST_FULL = 5.0
|
if not self.initialized:
|
||||||
MODEL_DROP_TRUST_NONE = 30.0
|
self.x = measurement
|
||||||
MODEL_TRUST_MIN = 0.5
|
self.initialized = True
|
||||||
|
self.confidence = 0.1
|
||||||
|
return
|
||||||
|
|
||||||
CREEP_SPEED_ENTER = 2.0
|
self.P = self.alpha * self.P + self.Q
|
||||||
CREEP_SPEED_EXIT = 3.0
|
|
||||||
|
|
||||||
ENTER_FRAMES = 3
|
K = self.P / (self.P + self.R)
|
||||||
EXIT_FRAMES = 16
|
effective_K = K * (1.0 - self.smoothing_factor) + self.smoothing_factor * 0.1
|
||||||
MIN_BLENDED_FRAMES = 20
|
|
||||||
|
|
||||||
PARAM_READ_FRAMES = 5
|
innovation = measurement - self.x
|
||||||
|
self.x = self.x + effective_K * innovation
|
||||||
|
self.P = (1 - effective_K) * self.P
|
||||||
|
|
||||||
|
if abs(innovation) < 0.1:
|
||||||
@dataclass
|
self.confidence = min(1.0, self.confidence + 0.05)
|
||||||
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
|
|
||||||
slowdown_detected = not degraded and s.decel_intent >= DECEL_INTENT_TRIGGER and not s.curve_detected
|
|
||||||
return slowdown_detected or s.creeping
|
|
||||||
|
|
||||||
|
|
||||||
class ModeHysteresis:
|
|
||||||
def __init__(self):
|
|
||||||
self.mode: ModeType = 'acc'
|
|
||||||
self.above = 0
|
|
||||||
self.below = 0
|
|
||||||
self.blended_frames = 0
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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:
|
else:
|
||||||
self.blended_frames += 1
|
self.confidence = max(0.1, self.confidence - 0.02)
|
||||||
if self.blended_frames >= MIN_BLENDED_FRAMES and self.below >= EXIT_FRAMES:
|
|
||||||
self.mode = 'acc'
|
|
||||||
return self.mode
|
|
||||||
|
|
||||||
def reset(self) -> None:
|
def get_value(self):
|
||||||
self.mode = 'acc'
|
return self.x if self.initialized else None
|
||||||
self.above = 0
|
|
||||||
self.below = 0
|
def get_confidence(self):
|
||||||
self.blended_frames = 0
|
return self.confidence
|
||||||
|
|
||||||
|
def reset_data(self):
|
||||||
|
self.initialized = False
|
||||||
|
self.history = []
|
||||||
|
self.confidence = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
class ModeTransitionManager:
|
||||||
|
"""Manages smooth transitions between driving modes with hysteresis."""
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
class DynamicExperimentalController:
|
class DynamicExperimentalController:
|
||||||
def __init__(self, CP: structs.CarParams, mpc, params=None):
|
def __init__(self, CP: structs.CarParams, mpc, params=None):
|
||||||
|
self._CP = CP
|
||||||
self._mpc = mpc
|
self._mpc = mpc
|
||||||
self._params = params or Params()
|
self._params = params or Params()
|
||||||
self._enabled: bool = self._params.get_bool("DynamicExperimentalControl")
|
self._enabled: bool = self._params.get_bool("DynamicExperimentalControl")
|
||||||
self._active: bool = False
|
self._active: bool = False
|
||||||
self._frame: int = 0
|
self._frame: int = 0
|
||||||
|
self._urgency = 0.0
|
||||||
|
|
||||||
self._hysteresis = ModeHysteresis()
|
self._mode_manager = ModeTransitionManager()
|
||||||
self._creeping = False
|
|
||||||
|
|
||||||
self.signals = DecSignals()
|
# Smooth filters for stable decision making with faster response for critical scenarios
|
||||||
self.want_blended = False
|
self._lead_filter = SmoothKalmanFilter(
|
||||||
self.lead_veto = False
|
measurement_noise=0.15,
|
||||||
|
process_noise=0.05,
|
||||||
|
alpha=1.02,
|
||||||
|
smoothing_factor=0.8
|
||||||
|
)
|
||||||
|
|
||||||
def _update_creeping(self, v_ego: float) -> bool:
|
self._slow_down_filter = SmoothKalmanFilter(
|
||||||
self._creeping = v_ego < CREEP_SPEED_EXIT if self._creeping else v_ego <= CREEP_SPEED_ENTER
|
measurement_noise=0.1,
|
||||||
return self._creeping
|
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 _read_params(self) -> None:
|
def _read_params(self) -> None:
|
||||||
if self._frame % PARAM_READ_FRAMES == 0:
|
if self._frame % int(1. / DT_MDL) == 0:
|
||||||
self._enabled = self._params.get_bool("DynamicExperimentalControl")
|
self._enabled = self._params.get_bool("DynamicExperimentalControl")
|
||||||
|
|
||||||
def mode(self) -> str:
|
def mode(self) -> str:
|
||||||
return self._hysteresis.mode
|
return self._mode_manager.get_mode()
|
||||||
|
|
||||||
def enabled(self) -> bool:
|
def enabled(self) -> bool:
|
||||||
return self._enabled
|
return self._enabled
|
||||||
@@ -121,61 +197,192 @@ class DynamicExperimentalController:
|
|||||||
def active(self) -> bool:
|
def active(self) -> bool:
|
||||||
return self._active
|
return self._active
|
||||||
|
|
||||||
@staticmethod
|
def set_mpc_fcw_crash_cnt(self) -> None:
|
||||||
def _decel_intent(md) -> float:
|
"""Set MPC FCW crash count"""
|
||||||
v = np.asarray(md.velocity.x)
|
self._mpc_fcw_crash_cnt = self._mpc.crash_cnt
|
||||||
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]))
|
|
||||||
|
|
||||||
@staticmethod
|
def _update_calculations(self, sm: messaging.SubMaster) -> None:
|
||||||
def _curve_detected(md) -> bool:
|
car_state = sm['carState']
|
||||||
y = md.position.y
|
lead_one = sm['radarState'].leadOne
|
||||||
if len(y) < 1:
|
md = sm['modelV2']
|
||||||
return False
|
|
||||||
return abs(y[-1]) >= CURVE_Y_MAX
|
|
||||||
|
|
||||||
@staticmethod
|
self._v_ego_kph = car_state.vEgo * 3.6
|
||||||
def _model_trust(md) -> float:
|
self._v_cruise_kph = car_state.vCruise
|
||||||
if len(md.velocity.x) != len(_T_IDXS):
|
self._has_standstill = car_state.standstill
|
||||||
return 0.0
|
|
||||||
return float(np.interp(md.frameDropPerc, [MODEL_DROP_TRUST_FULL, MODEL_DROP_TRUST_NONE], [1.0, 0.0]))
|
|
||||||
|
|
||||||
@staticmethod
|
# standstill detection
|
||||||
def _lead_veto(radar_state, md) -> bool:
|
if self._has_standstill:
|
||||||
lead_one, lead_two = radar_state.leadOne, radar_state.leadTwo
|
self._standstill_count = min(20, self._standstill_count + 1)
|
||||||
lead_now = lead_one.present or lead_two.present
|
else:
|
||||||
probs = md.leadsV3
|
self._standstill_count = max(0, self._standstill_count - 1)
|
||||||
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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
def update(self, sm: messaging.SubMaster) -> None:
|
def update(self, sm: messaging.SubMaster) -> None:
|
||||||
self._read_params()
|
self._read_params()
|
||||||
|
|
||||||
car_state = sm['carState']
|
self.set_mpc_fcw_crash_cnt()
|
||||||
md = sm['modelV2']
|
|
||||||
radar_state = sm['radarState']
|
|
||||||
|
|
||||||
is_creeping = self._update_creeping(car_state.vEgo)
|
self._update_calculations(sm)
|
||||||
self.lead_veto = self._lead_veto(radar_state, md)
|
|
||||||
|
|
||||||
self.signals = DecSignals(
|
if self._CP.radarUnavailable:
|
||||||
decel_intent=self._decel_intent(md),
|
self._radarless_mode()
|
||||||
curve_detected=self._curve_detected(md),
|
|
||||||
model_trust=self._model_trust(md),
|
|
||||||
creeping=is_creeping,
|
|
||||||
)
|
|
||||||
self.want_blended = should_blend(self.signals)
|
|
||||||
|
|
||||||
crash_override = self._mpc.crash_cnt >= 1
|
|
||||||
hard_brake_override = bool(md.meta.hardBrakePredicted)
|
|
||||||
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:
|
else:
|
||||||
self._hysteresis.reset()
|
self._radar_mode()
|
||||||
|
|
||||||
|
self._mode_manager.update()
|
||||||
self._active = sm['selfdriveState'].experimentalMode and self._enabled
|
self._active = sm['selfdriveState'].experimentalMode and self._enabled
|
||||||
self._frame += 1
|
self._frame += 1
|
||||||
|
|||||||
@@ -1,285 +1,91 @@
|
|||||||
import numpy as np
|
|
||||||
|
|
||||||
from openpilot.cereal import messaging
|
|
||||||
from opendbc.car import structs
|
|
||||||
from openpilot.common.test import OpenpilotTestCase
|
from openpilot.common.test import OpenpilotTestCase
|
||||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import (
|
|
||||||
DecSignals,
|
|
||||||
DynamicExperimentalController,
|
|
||||||
ModeHysteresis,
|
|
||||||
should_blend,
|
|
||||||
ENTER_FRAMES,
|
|
||||||
MIN_BLENDED_FRAMES,
|
|
||||||
)
|
|
||||||
|
|
||||||
T_IDXS = np.array(ModelConstants.T_IDXS)
|
class MockLeadOne:
|
||||||
|
def __init__(self, present=0.0):
|
||||||
|
self.present = present
|
||||||
|
|
||||||
|
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:
|
class MockParams:
|
||||||
def __init__(self, enabled=True):
|
|
||||||
self._enabled = enabled
|
|
||||||
|
|
||||||
def get_bool(self, name):
|
def get_bool(self, name):
|
||||||
return self._enabled
|
return True
|
||||||
|
|
||||||
|
def default_sm():
|
||||||
class MockMpc:
|
sm = {
|
||||||
def __init__(self, crash_cnt=0):
|
'carState': MockCarState(vEgo=10.0, vCruise=20.0),
|
||||||
self.crash_cnt = crash_cnt
|
'radarState': MockRadarState(present=1.0),
|
||||||
|
'modelV2': MockModelData(valid=True),
|
||||||
|
'selfdriveState': MockSelfDriveState(experimentalMode=True),
|
||||||
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 make_controller(cp=None, mpc=None, enabled=True):
|
def mock_mpc():
|
||||||
return DynamicExperimentalController(cp or structs.CarParams(), mpc or MockMpc(), params=MockParams(enabled))
|
class MPC:
|
||||||
|
crash_cnt = 0
|
||||||
|
return MPC()
|
||||||
|
|
||||||
|
# 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):
|
class TestDynamicExperimentalController(OpenpilotTestCase):
|
||||||
def test_initial_mode_is_acc(self):
|
def test_initial_mode_is_acc(self, mock_cp, mock_mpc):
|
||||||
controller = make_controller()
|
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||||
assert controller.mode() == "acc"
|
assert controller.mode() == "acc"
|
||||||
|
|
||||||
def test_flat_plan_never_blends_at_any_speed(self):
|
def test_standstill_triggers_blended(self, mock_cp, mock_mpc, default_sm):
|
||||||
for v_ego in (2.5, 5.6, 8.3, 13.9, 22.2, 30.6):
|
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||||
controller = make_controller()
|
default_sm['carState'].standstill = True
|
||||||
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):
|
for _ in range(10):
|
||||||
controller.update(sm)
|
controller.update(default_sm)
|
||||||
assert controller.mode() == "blended"
|
assert controller.mode() == "blended"
|
||||||
|
|
||||||
def test_curve_exclusion_prevents_false_blend(self):
|
def test_emergency_blended_on_fcw(self, mock_cp, mock_mpc, default_sm):
|
||||||
controller = make_controller()
|
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||||
sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -2.0), position_y=[6.0] * len(T_IDXS))
|
mock_mpc.crash_cnt = 1 # simulate FCW
|
||||||
for _ in range(30):
|
for _ in range(2):
|
||||||
controller.update(sm)
|
controller.update(default_sm)
|
||||||
assert controller.mode() == "acc"
|
|
||||||
|
|
||||||
def test_any_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(60):
|
|
||||||
controller.update(sm)
|
|
||||||
assert controller.mode() == "acc"
|
|
||||||
|
|
||||||
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"
|
assert controller.mode() == "blended"
|
||||||
|
|
||||||
def test_lead_gone_with_no_underlying_slowdown_stays_acc(self):
|
def test_radarless_slowdown_triggers_blended(self, mock_cp, mock_mpc, default_sm):
|
||||||
controller = make_controller()
|
mock_cp.radarUnavailable = True
|
||||||
lead_sm = make_sm(v_ego=20.0, velocity=flat_velocity(20.0), lead_present=True, lead_probs=[1.0, 1.0, 1.0])
|
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||||
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)
|
# Force conditions to simulate slowdown
|
||||||
for _ in range(20):
|
controller._slow_down_filter = FakeKalman(value=1.0) # ty: ignore[invalid-assignment]
|
||||||
controller.update(no_lead_sm)
|
controller._v_ego_kph = 35.0
|
||||||
assert controller.mode() == "acc"
|
default_sm['modelV2'] = MockModelData(valid=False) # Incomplete trajectory
|
||||||
|
|
||||||
def test_creep_does_not_release_lead_veto(self):
|
for _ in range(3):
|
||||||
controller = make_controller()
|
controller.update(default_sm)
|
||||||
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_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"
|
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_hard_brake_override_inert_while_lead_present(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])
|
|
||||||
controller.update(sm)
|
|
||||||
assert controller.mode() == "acc"
|
|
||||||
|
|
||||||
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=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,115 +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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from collections import deque
|
|
||||||
import math
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from openpilot.cereal import log
|
|
||||||
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState
|
|
||||||
|
|
||||||
|
|
||||||
LEAD_DEPARTURE_MIN_SPEED = 0.3
|
|
||||||
LEAD_DEPARTURE_CONFIRM_FRAMES = 3
|
|
||||||
LEAD_DEPARTURE_MIN_DISTANCE = 0.03
|
|
||||||
LEAD_DEPARTURE_MAX_EGO_SPEED = 0.3
|
|
||||||
|
|
||||||
MpcPlanSource = log.LongitudinalPlan.LongitudinalPlanSource
|
|
||||||
|
|
||||||
|
|
||||||
class LeadDepartureController:
|
|
||||||
def __init__(self, enabled: bool):
|
|
||||||
self.enabled = enabled
|
|
||||||
self._track_id: int | None = None
|
|
||||||
self._distances: deque[float] = deque(maxlen=LEAD_DEPARTURE_CONFIRM_FRAMES)
|
|
||||||
self._active = False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def active(self) -> bool:
|
|
||||||
return self._active
|
|
||||||
|
|
||||||
def reset(self) -> None:
|
|
||||||
self._track_id = None
|
|
||||||
self._distances.clear()
|
|
||||||
self._active = False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _selected_lead(radar_state: Any, source: Any) -> Any | None:
|
|
||||||
if source == MpcPlanSource.lead0:
|
|
||||||
return radar_state.leadOne
|
|
||||||
if source == MpcPlanSource.lead1:
|
|
||||||
return radar_state.leadTwo
|
|
||||||
return None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _radar_has_errors(radar_state: Any) -> bool:
|
|
||||||
errors = radar_state.radarErrors
|
|
||||||
return errors.canError or errors.radarFault or errors.wrongConfig or errors.radarUnavailableTemporary
|
|
||||||
|
|
||||||
def update(self, sm: Any, source: Any, a_target: float, should_stop: bool, reset: bool, radar_valid: bool) -> bool:
|
|
||||||
CS = sm['carState']
|
|
||||||
CC = sm['carControl']
|
|
||||||
controls_state = sm['controlsState']
|
|
||||||
radar_state = sm['radarState']
|
|
||||||
|
|
||||||
blocked = (
|
|
||||||
not self.enabled
|
|
||||||
or reset
|
|
||||||
or not CC.longActive
|
|
||||||
or CC.cruiseControl.override
|
|
||||||
or CS.gasPressed
|
|
||||||
or CS.brakePressed
|
|
||||||
or controls_state.forceDecel
|
|
||||||
or controls_state.longControlState == LongCtrlState.off
|
|
||||||
or not radar_valid
|
|
||||||
or self._radar_has_errors(radar_state)
|
|
||||||
)
|
|
||||||
if blocked or not math.isfinite(CS.vEgo) or CS.vEgo >= LEAD_DEPARTURE_MAX_EGO_SPEED or not math.isfinite(a_target):
|
|
||||||
self.reset()
|
|
||||||
return should_stop
|
|
||||||
|
|
||||||
lead = self._selected_lead(radar_state, source)
|
|
||||||
lead_valid = (
|
|
||||||
lead is not None
|
|
||||||
and lead.present
|
|
||||||
and lead.radar
|
|
||||||
and lead.radarTrackId >= 0
|
|
||||||
and all(math.isfinite(value) for value in (lead.dRel, lead.vLeadK, lead.vRel))
|
|
||||||
and lead.dRel > 0.0
|
|
||||||
and lead.vLeadK >= LEAD_DEPARTURE_MIN_SPEED
|
|
||||||
and lead.vRel >= LEAD_DEPARTURE_MIN_SPEED
|
|
||||||
and a_target >= 0.0
|
|
||||||
)
|
|
||||||
if not lead_valid:
|
|
||||||
self.reset()
|
|
||||||
return should_stop
|
|
||||||
|
|
||||||
track_id = int(lead.radarTrackId)
|
|
||||||
if self._active:
|
|
||||||
if track_id != self._track_id:
|
|
||||||
self.reset()
|
|
||||||
return should_stop
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not should_stop:
|
|
||||||
self.reset()
|
|
||||||
return False
|
|
||||||
|
|
||||||
if controls_state.longControlState != LongCtrlState.stopping:
|
|
||||||
self.reset()
|
|
||||||
return should_stop
|
|
||||||
|
|
||||||
if track_id != self._track_id:
|
|
||||||
self._track_id = track_id
|
|
||||||
self._distances.clear()
|
|
||||||
self._distances.append(float(lead.dRel))
|
|
||||||
|
|
||||||
if len(self._distances) == LEAD_DEPARTURE_CONFIRM_FRAMES and self._distances[-1] - self._distances[0] >= LEAD_DEPARTURE_MIN_DISTANCE:
|
|
||||||
self._active = True
|
|
||||||
return False
|
|
||||||
|
|
||||||
return should_stop
|
|
||||||
@@ -1,101 +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 math
|
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
from opendbc.car import DT_CTRL
|
|
||||||
|
|
||||||
STOPPING_DISTANCE = 0.75
|
|
||||||
STOPPING_TIME = 2.5
|
|
||||||
STOPPING_ACCEL_TOLERANCE = 0.1
|
|
||||||
STOPPING_SPEED_TOLERANCE = 0.05
|
|
||||||
STOPPING_SETTLE_FRAMES = 30
|
|
||||||
STOPPING_HOLD_ACCEL = -1.2
|
|
||||||
STOPPING_HOLD_MARGIN = 0.6
|
|
||||||
STOPPING_HOLD_SPEED_TOLERANCE = 0.01
|
|
||||||
|
|
||||||
|
|
||||||
class LongControlSP:
|
|
||||||
def __init__(self):
|
|
||||||
self._stopping_settle_frames: int | None = None
|
|
||||||
self._stopping_hold_accel: float | None = None
|
|
||||||
|
|
||||||
def _hold_supported(self) -> bool:
|
|
||||||
return self.CP.openpilotLongitudinalControl and not self.CP.notCar and self.CP.stopAccel < 0.0
|
|
||||||
|
|
||||||
def update_state(self, stopping: bool, active: bool, CS) -> None:
|
|
||||||
if not active:
|
|
||||||
self._stopping_settle_frames = None
|
|
||||||
self._stopping_hold_accel = None
|
|
||||||
return
|
|
||||||
|
|
||||||
invalid_speed = not all(math.isfinite(speed) for speed in (CS.vEgo, CS.vEgoRaw))
|
|
||||||
moving = max(abs(CS.vEgo), abs(CS.vEgoRaw)) > STOPPING_SPEED_TOLERANCE
|
|
||||||
if invalid_speed or (not stopping and moving):
|
|
||||||
self._stopping_hold_accel = None
|
|
||||||
elif (self._hold_supported() and math.isfinite(self.last_output_accel)
|
|
||||||
and self.last_output_accel <= self.CP.stopAccel):
|
|
||||||
previous_hold = self._stopping_hold_accel if self._stopping_hold_accel is not None else self.last_output_accel
|
|
||||||
self._stopping_hold_accel = min(self.last_output_accel, previous_hold)
|
|
||||||
if not stopping:
|
|
||||||
self._stopping_settle_frames = None
|
|
||||||
if self._stopping_hold_accel is not None and math.isfinite(self.last_output_accel):
|
|
||||||
self._stopping_hold_accel = min(self.last_output_accel, self._stopping_hold_accel)
|
|
||||||
|
|
||||||
def stopping_accel(self, output_accel: float, CS) -> float:
|
|
||||||
if self._stopping_hold_accel is not None and math.isfinite(CS.vEgo) and abs(CS.vEgo) <= STOPPING_SPEED_TOLERANCE:
|
|
||||||
return min(output_accel, self._stopping_hold_accel)
|
|
||||||
return output_accel
|
|
||||||
|
|
||||||
def stopping_decel_rate(self, CS, a_target: float, output_accel: float) -> float:
|
|
||||||
if not all(math.isfinite(value) for value in (output_accel, a_target, CS.vEgo, CS.vEgoRaw, CS.aEgo)):
|
|
||||||
return 1.0
|
|
||||||
hold_supported = self._hold_supported()
|
|
||||||
preserving_hold = self._stopping_hold_accel is not None
|
|
||||||
can_hold = output_accel <= 0.0 and a_target >= output_accel
|
|
||||||
terminal_speed = (0.0 <= CS.vEgo <= STOPPING_SPEED_TOLERANCE
|
|
||||||
or CS.standstill and abs(CS.vEgo) <= STOPPING_SPEED_TOLERANCE)
|
|
||||||
positive_stop_entry = self.last_output_accel > 0.0 and output_accel == 0.0
|
|
||||||
if output_accel > 0.0 or positive_stop_entry or CS.vEgo < 0.0 and not terminal_speed:
|
|
||||||
return 1.0
|
|
||||||
if terminal_speed and self._stopping_settle_frames is None:
|
|
||||||
if not preserving_hold and (not can_hold or output_accel > -STOPPING_ACCEL_TOLERANCE or CS.aEgo >= -STOPPING_ACCEL_TOLERANCE):
|
|
||||||
return 1.0
|
|
||||||
self._stopping_settle_frames = 0
|
|
||||||
|
|
||||||
time_decel = 0.0 if self._stopping_settle_frames is not None else CS.vEgo / STOPPING_TIME
|
|
||||||
required_decel = max(time_decel, CS.vEgo ** 2 / (2.0 * STOPPING_DISTANCE), 1e-3)
|
|
||||||
adequacy = min(max(-CS.aEgo / required_decel, 0.0), 1.0)
|
|
||||||
planner_need = min(max((output_accel - a_target) / max(required_decel, STOPPING_ACCEL_TOLERANCE), 0.0), 1.0)
|
|
||||||
if not terminal_speed and self._stopping_settle_frames is None and can_hold and adequacy >= 1.0:
|
|
||||||
self._stopping_settle_frames = 0
|
|
||||||
if hold_supported:
|
|
||||||
self._stopping_hold_accel = output_accel
|
|
||||||
|
|
||||||
motion_need = 1.0 - adequacy ** 2
|
|
||||||
terminal_need = 0.0
|
|
||||||
if terminal_speed or self._stopping_settle_frames not in (None, 0):
|
|
||||||
settle_frames = cast(int, self._stopping_settle_frames)
|
|
||||||
self._stopping_settle_frames = min(settle_frames + 1, STOPPING_SETTLE_FRAMES)
|
|
||||||
terminal_need = (self._stopping_settle_frames / STOPPING_SETTLE_FRAMES) ** 2
|
|
||||||
|
|
||||||
if preserving_hold and self._stopping_hold_accel is not None:
|
|
||||||
self._stopping_hold_accel = min(output_accel, self._stopping_hold_accel)
|
|
||||||
if terminal_speed:
|
|
||||||
minimum_hold = min(STOPPING_HOLD_ACCEL, self.CP.stopAccel + STOPPING_HOLD_MARGIN)
|
|
||||||
hold_target = max(self.CP.stopAccel, min(minimum_hold, self._stopping_hold_accel))
|
|
||||||
if CS.aEgo > STOPPING_ACCEL_TOLERANCE or abs(CS.vEgoRaw) > STOPPING_HOLD_SPEED_TOLERANCE:
|
|
||||||
return 1.0
|
|
||||||
hold_rate = max(planner_need, terminal_need)
|
|
||||||
if CS.vEgoRaw == 0.0 and abs(CS.vEgo) <= STOPPING_HOLD_SPEED_TOLERANCE:
|
|
||||||
if output_accel <= hold_target:
|
|
||||||
return planner_need
|
|
||||||
hold_rate = max(planner_need, min(hold_rate, (output_accel - hold_target) / DT_CTRL))
|
|
||||||
return hold_rate
|
|
||||||
|
|
||||||
return max(motion_need, planner_need, terminal_need)
|
|
||||||
@@ -9,10 +9,8 @@ from openpilot.cereal import messaging, custom
|
|||||||
from opendbc.car import structs
|
from opendbc.car import structs
|
||||||
from openpilot.common.constants import CV
|
from openpilot.common.constants import CV
|
||||||
from openpilot.selfdrive.car.cruise import V_CRUISE_MAX
|
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.dec.dec import DynamicExperimentalController
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.e2e_alerts_helper import E2EAlertsHelper
|
from openpilot.sunnypilot.selfdrive.controls.lib.e2e_alerts_helper import E2EAlertsHelper
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.lead_departure_controller import LeadDepartureController
|
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.smart_cruise_control import SmartCruiseControl
|
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.smart_cruise_control import SmartCruiseControl
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_assist import SpeedLimitAssist
|
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_assist import SpeedLimitAssist
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_resolver import SpeedLimitResolver
|
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_resolver import SpeedLimitResolver
|
||||||
@@ -25,9 +23,8 @@ LongitudinalPlanSource = custom.LongitudinalPlanSP.LongitudinalPlanSource
|
|||||||
|
|
||||||
class LongitudinalPlannerSP:
|
class LongitudinalPlannerSP:
|
||||||
def __init__(self, CP: structs.CarParams, CP_SP: structs.CarParamsSP, mpc):
|
def __init__(self, CP: structs.CarParams, CP_SP: structs.CarParamsSP, mpc):
|
||||||
self.accel_controller = AccelController(mpc.dt)
|
|
||||||
self.lead_departure_controller = LeadDepartureController(CP.openpilotLongitudinalControl and CP.autoResumeSng and not CP.notCar)
|
|
||||||
self.events_sp = EventsSP()
|
self.events_sp = EventsSP()
|
||||||
|
self.resolver = SpeedLimitResolver()
|
||||||
self.dec = DynamicExperimentalController(CP, mpc)
|
self.dec = DynamicExperimentalController(CP, mpc)
|
||||||
self.scc = SmartCruiseControl()
|
self.scc = SmartCruiseControl()
|
||||||
self.resolver = SpeedLimitResolver()
|
self.resolver = SpeedLimitResolver()
|
||||||
@@ -46,23 +43,6 @@ class LongitudinalPlannerSP:
|
|||||||
|
|
||||||
return experimental_mode and self.dec.mode() == "blended"
|
return experimental_mode and self.dec.mode() == "blended"
|
||||||
|
|
||||||
def get_max_accel_override(self, v_ego: float) -> float | None:
|
|
||||||
if not self.accel_controller.is_enabled():
|
|
||||||
return None
|
|
||||||
return self.accel_controller.get_max_accel(v_ego)
|
|
||||||
|
|
||||||
def get_min_accel_override(self, v_ego: float, e2e: bool, force_decel: bool) -> float | None:
|
|
||||||
if e2e or force_decel or not self.accel_controller.is_enabled():
|
|
||||||
return None
|
|
||||||
return self.accel_controller.get_min_accel(v_ego)
|
|
||||||
|
|
||||||
def update_allow_throttle(self, throttle_prob: float, low_speed_override: bool, threshold: float) -> bool:
|
|
||||||
return self.accel_controller.update_allow_throttle(throttle_prob, low_speed_override=low_speed_override, threshold=threshold)
|
|
||||||
|
|
||||||
def update_lead_departure(self, sm: messaging.SubMaster, a_target: float, should_stop: bool, reset: bool) -> bool:
|
|
||||||
radar_valid = sm.valid.get('radarState', False) and getattr(sm, 'alive', {}).get('radarState', False)
|
|
||||||
return self.lead_departure_controller.update(sm, self.mpc.source, a_target, should_stop, reset, radar_valid)
|
|
||||||
|
|
||||||
def update_targets(self, sm: messaging.SubMaster, v_ego: float, a_ego: float, v_cruise: float) -> tuple[float, float]:
|
def update_targets(self, sm: messaging.SubMaster, v_ego: float, a_ego: float, v_cruise: float) -> tuple[float, float]:
|
||||||
CS = sm['carState']
|
CS = sm['carState']
|
||||||
v_cruise_cluster_kph = min(CS.vCruiseCluster, V_CRUISE_MAX)
|
v_cruise_cluster_kph = min(CS.vCruiseCluster, V_CRUISE_MAX)
|
||||||
@@ -94,12 +74,9 @@ class LongitudinalPlannerSP:
|
|||||||
return self.output_v_target, self.output_a_target
|
return self.output_v_target, self.output_a_target
|
||||||
|
|
||||||
def update(self, sm: messaging.SubMaster) -> None:
|
def update(self, sm: messaging.SubMaster) -> None:
|
||||||
self.accel_controller.update(sm)
|
|
||||||
self.events_sp.clear()
|
self.events_sp.clear()
|
||||||
self.e2e_alerts_helper.update(sm, self.events_sp)
|
|
||||||
|
|
||||||
def update_dec(self, sm: messaging.SubMaster) -> None:
|
|
||||||
self.dec.update(sm)
|
self.dec.update(sm)
|
||||||
|
self.e2e_alerts_helper.update(sm, self.events_sp)
|
||||||
|
|
||||||
def publish_longitudinal_plan_sp(self, sm: messaging.SubMaster, pm: messaging.PubMaster) -> None:
|
def publish_longitudinal_plan_sp(self, sm: messaging.SubMaster, pm: messaging.PubMaster) -> None:
|
||||||
plan_sp_send = messaging.new_message('longitudinalPlanSP')
|
plan_sp_send = messaging.new_message('longitudinalPlanSP')
|
||||||
@@ -117,15 +94,6 @@ class LongitudinalPlannerSP:
|
|||||||
dec.state = DecState.blended if self.dec.mode() == 'blended' else DecState.acc
|
dec.state = DecState.blended if self.dec.mode() == 'blended' else DecState.acc
|
||||||
dec.enabled = self.dec.enabled()
|
dec.enabled = self.dec.enabled()
|
||||||
dec.active = self.dec.active()
|
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 = self.accel_controller.is_enabled()
|
|
||||||
accel_controller.active = self.accel_controller_active
|
|
||||||
accel_controller.profile = self.accel_controller.profile
|
|
||||||
|
|
||||||
# Smart Cruise Control
|
# Smart Cruise Control
|
||||||
smartCruiseControl = longitudinalPlanSP.smartCruiseControl
|
smartCruiseControl = longitudinalPlanSP.smartCruiseControl
|
||||||
|
|||||||
+11
-368
@@ -4,8 +4,6 @@ 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.
|
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.
|
See the LICENSE.md file in the root directory for more details.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from types import SimpleNamespace
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -17,23 +15,8 @@ from openpilot.common.params import Params
|
|||||||
from openpilot.common.realtime import DT_MDL
|
from openpilot.common.realtime import DT_MDL
|
||||||
from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET
|
from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET
|
||||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
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 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
|
from openpilot.common.test import OpenpilotTestCase
|
||||||
|
|
||||||
VisionState = custom.LongitudinalPlanSP.SmartCruiseControl.VisionState
|
VisionState = custom.LongitudinalPlanSP.SmartCruiseControl.VisionState
|
||||||
@@ -124,6 +107,7 @@ def generate_controlsState():
|
|||||||
|
|
||||||
|
|
||||||
class TestSmartCruiseControlVision(OpenpilotTestCase):
|
class TestSmartCruiseControlVision(OpenpilotTestCase):
|
||||||
|
|
||||||
def setup_method(self):
|
def setup_method(self):
|
||||||
self.params = Params()
|
self.params = Params()
|
||||||
self.reset_params()
|
self.reset_params()
|
||||||
@@ -137,377 +121,36 @@ class TestSmartCruiseControlVision(OpenpilotTestCase):
|
|||||||
def reset_params(self):
|
def reset_params(self):
|
||||||
self.params.put_bool("SmartCruiseControlVision", True, block=True)
|
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):
|
def test_initial_state(self):
|
||||||
assert self.scc_v.state == VisionState.disabled
|
assert self.scc_v.state == VisionState.disabled
|
||||||
assert not self.scc_v.is_active
|
assert not self.scc_v.is_active
|
||||||
assert self.scc_v.output_v_target == V_CRUISE_UNSET
|
assert self.scc_v.output_v_target == V_CRUISE_UNSET
|
||||||
assert self.scc_v.output_a_target == 0.0
|
assert self.scc_v.output_a_target == 0.
|
||||||
|
|
||||||
def test_system_disabled(self):
|
def test_system_disabled(self):
|
||||||
self.params.put_bool("SmartCruiseControlVision", False, block=True)
|
self.params.put_bool("SmartCruiseControlVision", False, block=True)
|
||||||
self.scc_v.enabled = self.params.get_bool("SmartCruiseControlVision")
|
self.scc_v.enabled = self.params.get_bool("SmartCruiseControlVision")
|
||||||
|
|
||||||
for _ in range(int(10.0 / DT_MDL)):
|
for _ in range(int(10. / DT_MDL)):
|
||||||
self.scc_v.update(self.sm, True, False, 0.0, 0.0, 0.0)
|
self.scc_v.update(self.sm, True, False, 0., 0., 0.)
|
||||||
assert self.scc_v.state == VisionState.disabled
|
assert self.scc_v.state == VisionState.disabled
|
||||||
assert not self.scc_v.is_active
|
assert not self.scc_v.is_active
|
||||||
|
|
||||||
def test_disabled(self):
|
def test_disabled(self):
|
||||||
for _ in range(int(10.0 / DT_MDL)):
|
for _ in range(int(10. / DT_MDL)):
|
||||||
self.scc_v.update(self.sm, False, False, 0.0, 0.0, 0.0)
|
self.scc_v.update(self.sm, False, False, 0., 0., 0.)
|
||||||
assert self.scc_v.state == VisionState.disabled
|
assert self.scc_v.state == VisionState.disabled
|
||||||
|
|
||||||
def test_transition_disabled_to_enabled(self):
|
def test_transition_disabled_to_enabled(self):
|
||||||
for _ in range(int(10.0 / DT_MDL)):
|
for _ in range(int(10. / DT_MDL)):
|
||||||
self.scc_v.update(self.sm, True, False, 0.0, 0.0, 0.0)
|
self.scc_v.update(self.sm, True, False, 0., 0., 0.)
|
||||||
assert self.scc_v.state == VisionState.enabled
|
assert self.scc_v.state == VisionState.enabled
|
||||||
|
|
||||||
def test_unconfirmed_release_holds_but_urgent_reentry_tightens(self):
|
@parameterized.expand([
|
||||||
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),
|
("p97_just_above_threshold", True),
|
||||||
("single_spike_filtered", False),
|
("single_spike_filtered", False),
|
||||||
("persistent_high_values", True),
|
("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):
|
def test_max_pred_lat_acc_uses_p97_and_threshold(self, case, should_enter):
|
||||||
n = len(ModelConstants.T_IDXS)
|
n = len(ModelConstants.T_IDXS)
|
||||||
th = float(_ENTERING_PRED_LAT_ACC_TH)
|
th = float(_ENTERING_PRED_LAT_ACC_TH)
|
||||||
|
|||||||
-110
@@ -1,110 +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 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
|
|
||||||
+61
-89
@@ -23,21 +23,25 @@ _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.
|
_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.
|
_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.
|
_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.
|
_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
|
_A_LAT_REG_MAX = 2. # Maximum lateral acceleration
|
||||||
|
|
||||||
_RELIEF_CONFIRMATION_FRAMES = max(1, int(round(0.5 / DT_MDL)))
|
_NO_OVERSHOOT_TIME_HORIZON = 4. # s. Time to use for velocity desired based on a_target when not overshooting.
|
||||||
_TARGET_TIGHTEN_CONFIRMATION_FRAMES = max(1, int(round(0.1 / DT_MDL)))
|
|
||||||
_TARGET_RELEASE_CONFIRMATION_FRAMES = max(1, int(round(0.15 / DT_MDL)))
|
# Lookup table for the minimum smooth deceleration during the ENTERING state
|
||||||
_TARGET_TIGHTEN_RATE = 5. # m/s^2
|
# depending on the actual maximum absolute lateral acceleration predicted on the turn ahead.
|
||||||
_TARGET_RELEASE_RATE = 1. # m/s^2
|
_ENTERING_SMOOTH_DECEL_V = [-0.2, -1.] # min decel value allowed on ENTERING state
|
||||||
_BELOW_EGO_TARGET_RELEASE_RATE = 3. # m/s^2
|
_ENTERING_SMOOTH_DECEL_BP = [1.3, 3.] # absolute value of lat acc ahead
|
||||||
_MIN_PRED_SPEED = 1. # m/s
|
|
||||||
_MIN_ACTIVATION_SPEED = 10. # m/s
|
# 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.
|
||||||
|
|
||||||
|
|
||||||
class SmartCruiseControlVision:
|
class SmartCruiseControlVision:
|
||||||
@@ -61,62 +65,14 @@ class SmartCruiseControlVision:
|
|||||||
self.state = VisionState.disabled
|
self.state = VisionState.disabled
|
||||||
self.current_lat_acc = 0.
|
self.current_lat_acc = 0.
|
||||||
self.max_pred_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:
|
def get_a_target_from_control(self) -> float:
|
||||||
return self.a_ego
|
return self.a_target
|
||||||
|
|
||||||
def get_v_target_from_control(self) -> float:
|
def get_v_target_from_control(self) -> float:
|
||||||
if self.is_active:
|
if self.is_active:
|
||||||
return self._filtered_v_target()
|
return max(self.v_target, MIN_V) + self.a_target * _NO_OVERSHOOT_TIME_HORIZON
|
||||||
|
|
||||||
self.tighten_frames = 0
|
|
||||||
self.release_frames = 0
|
|
||||||
return V_CRUISE_UNSET
|
return V_CRUISE_UNSET
|
||||||
|
|
||||||
def _update_params(self) -> None:
|
def _update_params(self) -> None:
|
||||||
@@ -126,27 +82,25 @@ class SmartCruiseControlVision:
|
|||||||
def _update_calculations(self, sm: messaging.SubMaster) -> None:
|
def _update_calculations(self, sm: messaging.SubMaster) -> None:
|
||||||
if not self.long_enabled:
|
if not self.long_enabled:
|
||||||
return
|
return
|
||||||
|
else:
|
||||||
|
rate_plan = np.array(np.abs(sm['modelV2'].orientationRate.z))
|
||||||
|
vel_plan = np.array(sm['modelV2'].velocity.x)
|
||||||
|
|
||||||
rate_plan = np.asarray(np.abs(sm['modelV2'].orientationRate.z), dtype=float)
|
self.current_lat_acc = self.v_ego ** 2 * abs(sm['controlsState'].curvature)
|
||||||
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)
|
|
||||||
|
|
||||||
self.current_lat_acc = self.v_ego ** 2 * abs(sm['controlsState'].curvature)
|
# get the maximum lat accel from the model
|
||||||
self.max_pred_lat_acc = 0.
|
predicted_lat_accels = rate_plan * vel_plan
|
||||||
self.v_target = V_CRUISE_UNSET
|
self.max_pred_lat_acc = np.percentile(predicted_lat_accels, 97)
|
||||||
if np.any(valid):
|
|
||||||
self.max_pred_lat_acc = float(np.percentile(rate_plan[valid] * vel_plan[valid], 97))
|
# get the maximum curve based on the current velocity
|
||||||
max_pred_curvature = float(np.percentile(rate_plan[valid] / vel_plan[valid], 97))
|
v_ego = max(self.v_ego, 0.1) # ensure a value greater than 0 for calculations
|
||||||
if max_pred_curvature > 0.:
|
max_curve = self.max_pred_lat_acc / (v_ego**2)
|
||||||
self.v_target = min(float((_A_LAT_REG_MAX / max_pred_curvature) ** 0.5), V_CRUISE_UNSET)
|
|
||||||
|
# Get the target velocity for the maximum curve
|
||||||
|
self.v_target = (_A_LAT_REG_MAX / max_curve) ** 0.5
|
||||||
|
|
||||||
def _update_state_machine(self) -> tuple[bool, bool]:
|
def _update_state_machine(self) -> tuple[bool, bool]:
|
||||||
# ENABLED, ENTERING, TURNING, LEAVING, OVERRIDING
|
# 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:
|
if self.state != VisionState.disabled:
|
||||||
# longitudinal and feature disable always have priority in a non-disabled state
|
# longitudinal and feature disable always have priority in a non-disabled state
|
||||||
if not self.long_enabled or not self.enabled:
|
if not self.long_enabled or not self.enabled:
|
||||||
@@ -158,7 +112,7 @@ class SmartCruiseControlVision:
|
|||||||
# ENABLED
|
# ENABLED
|
||||||
if self.state == VisionState.enabled:
|
if self.state == VisionState.enabled:
|
||||||
# Do not enter a turn control cycle if the speed is low.
|
# Do not enter a turn control cycle if the speed is low.
|
||||||
if self.v_ego <= _MIN_ACTIVATION_SPEED:
|
if self.v_ego <= MIN_V:
|
||||||
pass
|
pass
|
||||||
# If significant lateral acceleration is predicted ahead, then move to Entering turn state.
|
# If significant lateral acceleration is predicted ahead, then move to Entering turn state.
|
||||||
elif self.max_pred_lat_acc >= _ENTERING_PRED_LAT_ACC_TH:
|
elif self.max_pred_lat_acc >= _ENTERING_PRED_LAT_ACC_TH:
|
||||||
@@ -174,26 +128,23 @@ class SmartCruiseControlVision:
|
|||||||
# Transition to Turning if current lateral acceleration is over the threshold.
|
# Transition to Turning if current lateral acceleration is over the threshold.
|
||||||
if self.current_lat_acc >= _TURNING_LAT_ACC_TH:
|
if self.current_lat_acc >= _TURNING_LAT_ACC_TH:
|
||||||
self.state = VisionState.turning
|
self.state = VisionState.turning
|
||||||
# Begin releasing only after both current and predicted lateral acceleration stay clear.
|
# Abort if the predicted lateral acceleration drops
|
||||||
elif self.relief_frames >= _RELIEF_CONFIRMATION_FRAMES:
|
elif self.max_pred_lat_acc < _ABORT_ENTERING_PRED_LAT_ACC_TH:
|
||||||
self.state = VisionState.leaving
|
self.state = VisionState.enabled
|
||||||
|
|
||||||
# TURNING
|
# TURNING
|
||||||
elif self.state == VisionState.turning:
|
elif self.state == VisionState.turning:
|
||||||
# Transition out of Turning if current lateral acceleration drops below a threshold.
|
# Transition to Leaving if current lateral acceleration drops below a threshold.
|
||||||
if self.current_lat_acc <= _LEAVING_LAT_ACC_TH:
|
if self.current_lat_acc <= _LEAVING_LAT_ACC_TH:
|
||||||
self.state = VisionState.entering if self.max_pred_lat_acc >= _ENTERING_PRED_LAT_ACC_TH else VisionState.leaving
|
self.state = VisionState.leaving
|
||||||
|
|
||||||
# LEAVING
|
# LEAVING
|
||||||
elif self.state == VisionState.leaving:
|
elif self.state == VisionState.leaving:
|
||||||
# Transition back to Turning if current lateral acceleration goes back over the threshold.
|
# Transition back to Turning if current lateral acceleration goes back over the threshold.
|
||||||
if self.current_lat_acc >= _TURNING_LAT_ACC_TH:
|
if self.current_lat_acc >= _TURNING_LAT_ACC_TH:
|
||||||
self.state = VisionState.turning
|
self.state = VisionState.turning
|
||||||
# Start a new turn cycle immediately if another curve is predicted.
|
# Finish if current lateral acceleration goes below a threshold.
|
||||||
elif self.max_pred_lat_acc >= _ENTERING_PRED_LAT_ACC_TH:
|
elif self.current_lat_acc < _FINISH_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
|
self.state = VisionState.enabled
|
||||||
|
|
||||||
# DISABLED
|
# DISABLED
|
||||||
@@ -206,11 +157,32 @@ class SmartCruiseControlVision:
|
|||||||
|
|
||||||
enabled = self.state in ENABLED_STATES
|
enabled = self.state in ENABLED_STATES
|
||||||
active = self.state in ACTIVE_STATES
|
active = self.state in ACTIVE_STATES
|
||||||
if not active:
|
|
||||||
self.relief_frames = 0
|
|
||||||
|
|
||||||
return enabled, active
|
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,
|
def update(self, sm: messaging.SubMaster, long_enabled: bool, long_override: bool, v_ego: float, a_ego: float,
|
||||||
v_cruise_setpoint: float) -> None:
|
v_cruise_setpoint: float) -> None:
|
||||||
self.long_enabled = long_enabled
|
self.long_enabled = long_enabled
|
||||||
@@ -223,7 +195,7 @@ class SmartCruiseControlVision:
|
|||||||
self._update_calculations(sm)
|
self._update_calculations(sm)
|
||||||
|
|
||||||
self.is_enabled, self.is_active = self._update_state_machine()
|
self.is_enabled, self.is_active = self._update_state_machine()
|
||||||
self.a_target = self.a_ego
|
self.a_target = self._update_solution()
|
||||||
|
|
||||||
self.output_v_target = self.get_v_target_from_control()
|
self.output_v_target = self.get_v_target_from_control()
|
||||||
self.output_a_target = self.get_a_target_from_control()
|
self.output_a_target = self.get_a_target_from_control()
|
||||||
|
|||||||
@@ -1,266 +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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from types import SimpleNamespace
|
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
from openpilot.cereal import log
|
|
||||||
from openpilot.common.realtime import DT_MDL
|
|
||||||
from openpilot.common.test import OpenpilotTestCase
|
|
||||||
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState
|
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.lead_departure_controller import LeadDepartureController
|
|
||||||
from openpilot.sunnypilot.selfdrive.test.longitudinal_maneuvers.plant import PRIUS_TSS2_ROUTE_MODEL, PlantSP
|
|
||||||
|
|
||||||
|
|
||||||
MpcPlanSource = log.LongitudinalPlan.LongitudinalPlanSource
|
|
||||||
|
|
||||||
|
|
||||||
def make_lead(*, d_rel: float = 4.0, v_lead: float = 0.5, v_rel: float = 0.5, present: bool = True, radar: bool = True, track_id: int = 7):
|
|
||||||
return SimpleNamespace(dRel=d_rel, vLeadK=v_lead, vRel=v_rel, present=present, radar=radar, radarTrackId=track_id)
|
|
||||||
|
|
||||||
|
|
||||||
def make_sm(
|
|
||||||
*,
|
|
||||||
lead_one=None,
|
|
||||||
lead_two=None,
|
|
||||||
v_ego: float = 0.0,
|
|
||||||
long_active: bool = True,
|
|
||||||
long_state=LongCtrlState.stopping,
|
|
||||||
gas: bool = False,
|
|
||||||
brake: bool = False,
|
|
||||||
override: bool = False,
|
|
||||||
force_decel: bool = False,
|
|
||||||
radar_error: str | None = None,
|
|
||||||
):
|
|
||||||
errors = SimpleNamespace(canError=False, radarFault=False, wrongConfig=False, radarUnavailableTemporary=False)
|
|
||||||
if radar_error is not None:
|
|
||||||
setattr(errors, radar_error, True)
|
|
||||||
return {
|
|
||||||
'carState': SimpleNamespace(vEgo=v_ego, gasPressed=gas, brakePressed=brake),
|
|
||||||
'carControl': SimpleNamespace(longActive=long_active, cruiseControl=SimpleNamespace(override=override)),
|
|
||||||
'controlsState': SimpleNamespace(longControlState=long_state, forceDecel=force_decel),
|
|
||||||
'radarState': SimpleNamespace(leadOne=lead_one or make_lead(), leadTwo=lead_two or make_lead(track_id=8), radarErrors=errors),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def update(controller, sm, *, source=MpcPlanSource.lead0, a_target: float = 0.05, should_stop: bool = True, reset: bool = False, radar_valid: bool = True):
|
|
||||||
return controller.update(sm, source, a_target, should_stop, reset, radar_valid)
|
|
||||||
|
|
||||||
|
|
||||||
def activate(controller: LeadDepartureController):
|
|
||||||
assert update(controller, make_sm(lead_one=make_lead(d_rel=4.00)))
|
|
||||||
assert update(controller, make_sm(lead_one=make_lead(d_rel=4.01)))
|
|
||||||
assert not update(controller, make_sm(lead_one=make_lead(d_rel=4.04)))
|
|
||||||
assert controller.active
|
|
||||||
|
|
||||||
|
|
||||||
def run_closed_loop(controller_enabled: bool, gap: float, lead_speed, duration: float, model_should_stop: bool | None = None):
|
|
||||||
def observe_lead(_t, _name, truth):
|
|
||||||
truth.update(radar=True, radarTrackId=7)
|
|
||||||
return truth
|
|
||||||
|
|
||||||
def model_action(_t, _v_ego, _a_ego):
|
|
||||||
return 0.0, bool(model_should_stop)
|
|
||||||
|
|
||||||
plant = PlantSP(
|
|
||||||
lead_relevancy=True,
|
|
||||||
speed=0.0,
|
|
||||||
distance_lead=gap,
|
|
||||||
lead_observation_fn=observe_lead,
|
|
||||||
actuator_model=PRIUS_TSS2_ROUTE_MODEL,
|
|
||||||
run_long_control=True,
|
|
||||||
e2e=model_should_stop is not None,
|
|
||||||
model_action_fn=model_action if model_should_stop is not None else None,
|
|
||||||
)
|
|
||||||
plant.planner.lead_departure_controller.enabled = controller_enabled
|
|
||||||
|
|
||||||
original_update = plant.planner.update
|
|
||||||
|
|
||||||
def long_active_update(sm):
|
|
||||||
sm['carControl'].longActive = True
|
|
||||||
original_update(sm)
|
|
||||||
|
|
||||||
solver_resets = 0
|
|
||||||
original_reset = plant.planner.mpc.reset
|
|
||||||
|
|
||||||
def counted_reset(*args, **kwargs):
|
|
||||||
nonlocal solver_resets
|
|
||||||
if plant.planner.mpc.solution_status != 0:
|
|
||||||
solver_resets += 1
|
|
||||||
return original_reset(*args, **kwargs)
|
|
||||||
|
|
||||||
rows = []
|
|
||||||
active = []
|
|
||||||
with (
|
|
||||||
mock.patch.object(plant.planner, 'get_max_accel_override', return_value=None),
|
|
||||||
mock.patch.object(plant.planner, 'get_min_accel_override', return_value=None),
|
|
||||||
mock.patch.object(plant.planner, 'update', side_effect=long_active_update),
|
|
||||||
mock.patch.object(plant.planner.mpc, 'reset', side_effect=counted_reset),
|
|
||||||
):
|
|
||||||
for _ in range(round(duration / DT_MDL)):
|
|
||||||
t = plant.current_time
|
|
||||||
result = plant.step(v_lead=lead_speed(t), v_cruise=8.0)
|
|
||||||
rows.append(
|
|
||||||
(t, result['speed'], result['distance'], result['distance_lead'] - result['distance'], result['actuator_command'], result['should_stop'], result['fcw'])
|
|
||||||
)
|
|
||||||
active.append(plant.planner.lead_departure_controller.active)
|
|
||||||
|
|
||||||
return rows, active, solver_resets
|
|
||||||
|
|
||||||
|
|
||||||
def first_delay(rows, cue: float, column: int, predicate):
|
|
||||||
return next(row[0] - cue for row in rows if row[0] >= cue and predicate(row[column]))
|
|
||||||
|
|
||||||
|
|
||||||
class TestLeadDepartureController(OpenpilotTestCase):
|
|
||||||
def test_requires_three_coherent_radar_frames(self):
|
|
||||||
controller = LeadDepartureController(True)
|
|
||||||
assert update(controller, make_sm(lead_one=make_lead(d_rel=4.00)))
|
|
||||||
assert update(controller, make_sm(lead_one=make_lead(d_rel=4.01)))
|
|
||||||
assert not update(controller, make_sm(lead_one=make_lead(d_rel=4.04)))
|
|
||||||
assert controller.active
|
|
||||||
|
|
||||||
def test_distance_confirmation_uses_a_sliding_three_frame_window(self):
|
|
||||||
controller = LeadDepartureController(True)
|
|
||||||
for d_rel in (4.00, 4.01, 4.02, 4.03):
|
|
||||||
assert update(controller, make_sm(lead_one=make_lead(d_rel=d_rel)))
|
|
||||||
assert not controller.active
|
|
||||||
|
|
||||||
assert not update(controller, make_sm(lead_one=make_lead(d_rel=4.06)))
|
|
||||||
assert controller.active
|
|
||||||
|
|
||||||
def test_persistent_false_speed_cue_with_static_range_never_arms(self):
|
|
||||||
controller = LeadDepartureController(True)
|
|
||||||
for _ in range(10):
|
|
||||||
assert update(controller, make_sm(lead_one=make_lead(d_rel=4.0)))
|
|
||||||
assert not controller.active
|
|
||||||
|
|
||||||
def test_same_track_can_move_between_lead_slots(self):
|
|
||||||
controller = LeadDepartureController(True)
|
|
||||||
assert update(controller, make_sm(lead_one=make_lead(d_rel=4.00)), source=MpcPlanSource.lead0)
|
|
||||||
assert update(controller, make_sm(lead_two=make_lead(d_rel=4.01)), source=MpcPlanSource.lead1)
|
|
||||||
assert not update(controller, make_sm(lead_one=make_lead(d_rel=4.04)), source=MpcPlanSource.lead0)
|
|
||||||
|
|
||||||
def test_different_track_restarts_confirmation(self):
|
|
||||||
controller = LeadDepartureController(True)
|
|
||||||
assert update(controller, make_sm(lead_one=make_lead(d_rel=4.00, track_id=7)))
|
|
||||||
assert update(controller, make_sm(lead_one=make_lead(d_rel=4.02, track_id=7)))
|
|
||||||
assert update(controller, make_sm(lead_one=make_lead(d_rel=4.20, track_id=9)))
|
|
||||||
assert update(controller, make_sm(lead_one=make_lead(d_rel=4.22, track_id=9)))
|
|
||||||
assert not update(controller, make_sm(lead_one=make_lead(d_rel=4.24, track_id=9)))
|
|
||||||
|
|
||||||
def test_active_release_latches_through_native_threshold_churn(self):
|
|
||||||
controller = LeadDepartureController(True)
|
|
||||||
activate(controller)
|
|
||||||
sm = make_sm(lead_one=make_lead(d_rel=4.10), long_state=LongCtrlState.pid)
|
|
||||||
assert not update(controller, sm, a_target=0.12, should_stop=False)
|
|
||||||
assert not update(controller, sm, a_target=0.05, should_stop=True)
|
|
||||||
assert controller.active
|
|
||||||
|
|
||||||
def test_active_release_latches_across_same_track_source_churn(self):
|
|
||||||
controller = LeadDepartureController(True)
|
|
||||||
activate(controller)
|
|
||||||
sm = make_sm(lead_two=make_lead(d_rel=4.10), long_state=LongCtrlState.pid)
|
|
||||||
assert not update(controller, sm, source=MpcPlanSource.lead1)
|
|
||||||
assert controller.active
|
|
||||||
|
|
||||||
def test_active_release_cancels_on_invalid_state(self):
|
|
||||||
cases = (
|
|
||||||
('lead lost', make_sm(lead_one=make_lead(present=False))),
|
|
||||||
('vision lead', make_sm(lead_one=make_lead(radar=False))),
|
|
||||||
('track changed', make_sm(lead_one=make_lead(track_id=9))),
|
|
||||||
('lead too slow', make_sm(lead_one=make_lead(v_lead=0.29))),
|
|
||||||
('relative speed too low', make_sm(lead_one=make_lead(v_rel=0.29))),
|
|
||||||
('gas', make_sm(gas=True)),
|
|
||||||
('brake', make_sm(brake=True)),
|
|
||||||
('override', make_sm(override=True)),
|
|
||||||
('force decel', make_sm(force_decel=True)),
|
|
||||||
('long inactive', make_sm(long_active=False)),
|
|
||||||
('long control off', make_sm(long_state=LongCtrlState.off)),
|
|
||||||
('ego rolling', make_sm(v_ego=0.3)),
|
|
||||||
('radar CAN error', make_sm(radar_error='canError')),
|
|
||||||
('radar fault', make_sm(radar_error='radarFault')),
|
|
||||||
('radar config', make_sm(radar_error='wrongConfig')),
|
|
||||||
('radar unavailable', make_sm(radar_error='radarUnavailableTemporary')),
|
|
||||||
)
|
|
||||||
for name, sm in cases:
|
|
||||||
with self.subTest(name=name):
|
|
||||||
controller = LeadDepartureController(True)
|
|
||||||
activate(controller)
|
|
||||||
assert update(controller, sm)
|
|
||||||
assert not controller.active
|
|
||||||
|
|
||||||
def test_active_release_cancels_on_invalid_update_input(self):
|
|
||||||
cases = (('negative target', -0.01, False, True), ('reset', 0.05, True, True), ('radar invalid', 0.05, False, False))
|
|
||||||
for name, a_target, reset, radar_valid in cases:
|
|
||||||
with self.subTest(name=name):
|
|
||||||
controller = LeadDepartureController(True)
|
|
||||||
activate(controller)
|
|
||||||
assert update(controller, make_sm(), a_target=a_target, reset=reset, radar_valid=radar_valid)
|
|
||||||
assert not controller.active
|
|
||||||
|
|
||||||
def test_inactive_controller_arms_only_from_native_stop_and_stopping_state(self):
|
|
||||||
controller = LeadDepartureController(True)
|
|
||||||
for d_rel in (4.00, 4.02, 4.04):
|
|
||||||
assert not update(controller, make_sm(lead_one=make_lead(d_rel=d_rel)), should_stop=False)
|
|
||||||
for d_rel in (4.00, 4.02, 4.04):
|
|
||||||
assert update(controller, make_sm(lead_one=make_lead(d_rel=d_rel), long_state=LongCtrlState.pid))
|
|
||||||
assert not controller.active
|
|
||||||
|
|
||||||
def test_capability_gate_disables_controller(self):
|
|
||||||
controller = LeadDepartureController(False)
|
|
||||||
for d_rel in (4.00, 4.02, 4.04):
|
|
||||||
assert update(controller, make_sm(lead_one=make_lead(d_rel=d_rel)))
|
|
||||||
assert not controller.active
|
|
||||||
|
|
||||||
def test_closed_loop_departure_releases_earlier_without_a_safety_regression(self):
|
|
||||||
lead_accel = 0.31
|
|
||||||
cue = 1.0 + 0.4 / lead_accel
|
|
||||||
|
|
||||||
def lead_speed(t):
|
|
||||||
return 0.0 if t < 1.0 else min(5.0, lead_accel * (t - 1.0))
|
|
||||||
|
|
||||||
stock, stock_active, stock_resets = run_closed_loop(False, 3.81, lead_speed, 8.0)
|
|
||||||
controller, controller_active, controller_resets = run_closed_loop(True, 3.81, lead_speed, 8.0)
|
|
||||||
|
|
||||||
stock_release = first_delay(stock, cue, 5, lambda should_stop: not should_stop)
|
|
||||||
controller_release = first_delay(controller, cue, 5, lambda should_stop: not should_stop)
|
|
||||||
stock_motion = first_delay(stock, cue, 1, lambda speed: speed > 0.01)
|
|
||||||
controller_motion = first_delay(controller, cue, 1, lambda speed: speed > 0.01)
|
|
||||||
stock_v01 = first_delay(stock, cue, 1, lambda speed: speed > 0.1)
|
|
||||||
controller_v01 = first_delay(controller, cue, 1, lambda speed: speed > 0.1)
|
|
||||||
|
|
||||||
assert any(controller_active) and not any(stock_active)
|
|
||||||
assert stock_resets == controller_resets == 0
|
|
||||||
assert not any(row[6] for row in stock + controller)
|
|
||||||
assert controller_release <= stock_release - 1.0
|
|
||||||
assert controller_motion <= stock_motion - 0.1
|
|
||||||
assert controller_v01 <= stock_v01 - 0.1
|
|
||||||
assert min(row[3] for row in controller) >= min(row[3] for row in stock)
|
|
||||||
assert max(abs(right[4] - left[4]) for left, right in zip(controller, controller[1:], strict=False)) <= max(
|
|
||||||
abs(right[4] - left[4]) for left, right in zip(stock, stock[1:], strict=False)
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_model_stop_remains_authoritative(self):
|
|
||||||
def lead_speed(t):
|
|
||||||
return 0.0 if t < 1.0 else min(5.0, 0.8 * (t - 1.0))
|
|
||||||
|
|
||||||
rows, active, solver_resets = run_closed_loop(True, 4.0, lead_speed, 6.0, model_should_stop=True)
|
|
||||||
|
|
||||||
assert any(active)
|
|
||||||
assert solver_resets == 0
|
|
||||||
assert all(row[5] for row in rows)
|
|
||||||
assert all(row[1] == 0.0 and row[2] == 0.0 for row in rows)
|
|
||||||
assert not any(row[6] for row in rows)
|
|
||||||
|
|
||||||
def test_stationary_lead_remains_stock_identical(self):
|
|
||||||
stock, stock_active, stock_resets = run_closed_loop(False, 8.0, lambda _t: 0.0, 12.0)
|
|
||||||
controller, controller_active, controller_resets = run_closed_loop(True, 8.0, lambda _t: 0.0, 12.0)
|
|
||||||
|
|
||||||
assert stock == controller
|
|
||||||
assert not any(stock_active) and not any(controller_active)
|
|
||||||
assert stock_resets == controller_resets == 0
|
|
||||||
@@ -1,642 +0,0 @@
|
|||||||
import numpy as np
|
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
from opendbc.car import DT_CTRL, gen_empty_fingerprint, structs
|
|
||||||
from openpilot.common.parameterized import parameterized
|
|
||||||
from openpilot.common.test import OpenpilotTestCase
|
|
||||||
from opendbc.car.body.values import CAR as BODY
|
|
||||||
from opendbc.car.car_helpers import interfaces
|
|
||||||
from opendbc.car.ford.values import CAR as FORD
|
|
||||||
from opendbc.car.gm.values import CAR as GM
|
|
||||||
from opendbc.car.honda.values import CAR as HONDA
|
|
||||||
from opendbc.car.hyundai.values import CAR as HYUNDAI
|
|
||||||
from opendbc.car.rivian.values import CAR as RIVIAN
|
|
||||||
from opendbc.car.subaru.values import CAR as SUBARU
|
|
||||||
from opendbc.car.tesla.values import CAR as TESLA
|
|
||||||
from opendbc.car.toyota.values import CAR as TOYOTA
|
|
||||||
from opendbc.car.volkswagen.values import CAR as VOLKSWAGEN
|
|
||||||
from openpilot.selfdrive.controls.lib.drive_helpers import should_stop
|
|
||||||
from openpilot.selfdrive.controls.lib.longcontrol import LongControl, LongCtrlState
|
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.longcontrol import (
|
|
||||||
STOPPING_HOLD_ACCEL, STOPPING_HOLD_MARGIN, STOPPING_SETTLE_FRAMES, STOPPING_SPEED_TOLERANCE,
|
|
||||||
)
|
|
||||||
from openpilot.sunnypilot.selfdrive.test.longitudinal_maneuvers.plant import PRIUS_TSS2_ROUTE_MODEL, PlantSP
|
|
||||||
|
|
||||||
|
|
||||||
PRESERVED_HOLD_VEHICLES = (
|
|
||||||
FORD.FORD_ESCAPE_MK4,
|
|
||||||
GM.CHEVROLET_VOLT,
|
|
||||||
GM.CHEVROLET_BOLT_EUV,
|
|
||||||
HONDA.HONDA_CIVIC_2022,
|
|
||||||
HYUNDAI.HYUNDAI_SONATA,
|
|
||||||
SUBARU.SUBARU_ASCENT,
|
|
||||||
TESLA.TESLA_MODEL_3,
|
|
||||||
TOYOTA.TOYOTA_RAV4_TSS2,
|
|
||||||
VOLKSWAGEN.VOLKSWAGEN_ARTEON_MK1,
|
|
||||||
)
|
|
||||||
STOP_ACCEL_VEHICLES = (*PRESERVED_HOLD_VEHICLES, RIVIAN.RIVIAN_R1)
|
|
||||||
SETTLE_VEHICLES = (TOYOTA.TOYOTA_RAV4_TSS2, HONDA.HONDA_CIVIC_2022, VOLKSWAGEN.VOLKSWAGEN_ARTEON_MK1)
|
|
||||||
UNSUPPORTED_HOLD_VEHICLES = (
|
|
||||||
(BODY.COMMA_BODY, True),
|
|
||||||
(SUBARU.SUBARU_OUTBACK, True),
|
|
||||||
(HYUNDAI.HYUNDAI_SONATA, False),
|
|
||||||
(RIVIAN.RIVIAN_R1, True),
|
|
||||||
)
|
|
||||||
ROUTE_STOP_ONSETS = (
|
|
||||||
(0.280, -0.290, -0.220, -0.220),
|
|
||||||
(0.290, -0.497, -0.270, -0.302),
|
|
||||||
(0.464, -0.223, -0.264, -0.292),
|
|
||||||
(0.467, -0.582, -0.316, -0.359),
|
|
||||||
(0.530, -0.311, -0.309, -0.333),
|
|
||||||
(0.581, -0.467, -0.312, -0.352),
|
|
||||||
(0.398, -0.557, -0.311, -0.348),
|
|
||||||
(0.517, -0.290, -0.301, -0.327),
|
|
||||||
(0.312, -0.420, -0.271, -0.304),
|
|
||||||
(0.474, -0.509, -0.303, -0.347),
|
|
||||||
(0.241, -0.554, -0.573, -0.617),
|
|
||||||
(0.292, -0.154, -0.302, -0.326),
|
|
||||||
)
|
|
||||||
GRADE_HOLD_CASES = (
|
|
||||||
(-0.49, -1.40),
|
|
||||||
(0.00, -1.40),
|
|
||||||
(0.49, -1.40),
|
|
||||||
(0.75, -1.40),
|
|
||||||
(0.98, -1.65),
|
|
||||||
(1.25, -2.00),
|
|
||||||
(1.47, -2.00),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_car_params(candidate, experimental_long=True):
|
|
||||||
fingerprint = gen_empty_fingerprint()
|
|
||||||
interface = interfaces[candidate]
|
|
||||||
CP = interface.get_params(candidate, fingerprint, [], experimental_long, False, False)
|
|
||||||
return CP, interface.get_params_sp(CP, candidate, fingerprint, [], experimental_long, False, False)
|
|
||||||
|
|
||||||
|
|
||||||
def make_car_state(v_ego=0.2, a_ego=0.0, standstill=False, v_ego_raw=None) -> structs.CarState:
|
|
||||||
raw_speed = v_ego if v_ego_raw is None else v_ego_raw
|
|
||||||
state = structs.CarState(vEgo=float(v_ego), vEgoRaw=float(raw_speed), aEgo=float(a_ego), standstill=standstill)
|
|
||||||
state.cruiseState.standstill = standstill
|
|
||||||
return state
|
|
||||||
|
|
||||||
|
|
||||||
def make_control(candidate, initial_accel=-0.33, experimental_long=True):
|
|
||||||
CP, CP_SP = get_car_params(candidate, experimental_long)
|
|
||||||
control = LongControl(CP, CP_SP)
|
|
||||||
control.long_control_state = LongCtrlState.pid
|
|
||||||
control.last_output_accel = initial_accel
|
|
||||||
return CP, control
|
|
||||||
|
|
||||||
|
|
||||||
def stock_stopping_output(output_accel, stop_accel):
|
|
||||||
return min(output_accel, 0.0) - DT_CTRL if output_accel > stop_accel else output_accel
|
|
||||||
|
|
||||||
|
|
||||||
def expected_hold_accel(CP, initial_accel=-0.33):
|
|
||||||
minimum_hold = min(STOPPING_HOLD_ACCEL, CP.stopAccel + STOPPING_HOLD_MARGIN)
|
|
||||||
return min(initial_accel, max(CP.stopAccel, minimum_hold))
|
|
||||||
|
|
||||||
|
|
||||||
def settle_preserved_hold(control):
|
|
||||||
control.update(True, make_car_state(0.28, -0.29), -0.22, True, (-3.5, 2.0))
|
|
||||||
CS = make_car_state(0.0, 0.0, standstill=True)
|
|
||||||
for _ in range(round(4.0 / DT_CTRL)):
|
|
||||||
control.update(True, CS, -0.1, True, (-3.5, 2.0))
|
|
||||||
|
|
||||||
|
|
||||||
class TestLongControlSP(OpenpilotTestCase):
|
|
||||||
def test_stop_threshold_matches_the_shared_helper(self):
|
|
||||||
assert should_stop(0.29, 0.0)
|
|
||||||
assert not should_stop(0.3, 0.0)
|
|
||||||
assert not should_stop(0.29, 0.1)
|
|
||||||
|
|
||||||
def test_hold_scope_matches_every_car_interface(self):
|
|
||||||
for candidate in interfaces:
|
|
||||||
for experimental_long in (False, True):
|
|
||||||
with self.subTest(candidate=candidate, experimental_long=experimental_long):
|
|
||||||
CP, control = make_control(candidate, experimental_long=experimental_long)
|
|
||||||
output = control.update(True, make_car_state(0.28, -0.29), -0.22, True, (-3.5, 2.0))
|
|
||||||
supported = CP.openpilotLongitudinalControl and not CP.notCar and CP.stopAccel < 0.0
|
|
||||||
|
|
||||||
self.assertAlmostEqual(output, -0.33)
|
|
||||||
assert (control._stopping_hold_accel is not None) == supported
|
|
||||||
|
|
||||||
@parameterized.expand(UNSUPPORTED_HOLD_VEHICLES, names=("candidate", "experimental_long"))
|
|
||||||
def test_unsupported_hold_semantics_keep_the_cache_disabled(self, candidate, experimental_long):
|
|
||||||
_, control = make_control(candidate, experimental_long=experimental_long)
|
|
||||||
control.update(True, make_car_state(0.28, -0.29), -0.22, True, (-3.5, 2.0))
|
|
||||||
|
|
||||||
assert control._stopping_hold_accel is None
|
|
||||||
|
|
||||||
@parameterized.expand(ROUTE_STOP_ONSETS, names=("v_ego", "a_ego", "a_target", "initial_accel"))
|
|
||||||
def test_logged_stop_onsets_hold_the_existing_brake(self, v_ego, a_ego, a_target, initial_accel):
|
|
||||||
_, control = make_control(TOYOTA.TOYOTA_RAV4_TSS2, initial_accel)
|
|
||||||
output = control.update(True, make_car_state(v_ego, a_ego), a_target, True, (-3.5, 2.0))
|
|
||||||
assert control.long_control_state == LongCtrlState.stopping
|
|
||||||
self.assertAlmostEqual(output, initial_accel)
|
|
||||||
|
|
||||||
@parameterized.expand(ROUTE_STOP_ONSETS, names=("v_ego", "a_ego", "a_target", "initial_accel"))
|
|
||||||
def test_logged_stop_onsets_preserve_a_settled_hold(self, v_ego, a_ego, a_target, initial_accel):
|
|
||||||
CP, control = make_control(TOYOTA.TOYOTA_RAV4_TSS2, initial_accel)
|
|
||||||
control.update(True, make_car_state(v_ego, a_ego), a_target, True, (-3.5, 2.0))
|
|
||||||
CS = make_car_state(0.0, 0.0, standstill=True)
|
|
||||||
outputs = [control.update(True, CS, a_target, True, (-3.5, 2.0)) for _ in range(round(10.0 / DT_CTRL))]
|
|
||||||
|
|
||||||
hold_floor = expected_hold_accel(CP, initial_accel)
|
|
||||||
self.assertAlmostEqual(outputs[-1], hold_floor)
|
|
||||||
np.testing.assert_allclose(outputs[-100:], outputs[-1], rtol=0.0, atol=1e-12)
|
|
||||||
assert all(current <= previous for previous, current in zip(outputs[:-1], outputs[1:], strict=True))
|
|
||||||
|
|
||||||
@parameterized.expand(PRESERVED_HOLD_VEHICLES, names=("candidate",))
|
|
||||||
def test_preserved_hold_does_not_change_the_moving_approach(self, candidate):
|
|
||||||
CP, control = make_control(candidate)
|
|
||||||
control.update(True, make_car_state(0.28, -0.29), -0.22, True, (-3.5, 2.0))
|
|
||||||
moving = [control.update(True, make_car_state(0.25, -0.25), -0.22, True, (-3.5, 2.0)) for _ in range(20)]
|
|
||||||
CS = make_car_state(0.0, 0.0, standstill=True)
|
|
||||||
terminal = [control.update(True, CS, -0.1, True, (-3.5, 2.0)) for _ in range(round(4.0 / DT_CTRL))]
|
|
||||||
|
|
||||||
np.testing.assert_allclose(moving, -0.33, rtol=0.0, atol=1e-12)
|
|
||||||
self.assertAlmostEqual(terminal[-1], expected_hold_accel(CP))
|
|
||||||
|
|
||||||
def test_glide_hold_survives_a_soft_deceleration_sample(self):
|
|
||||||
_, control = make_control(TOYOTA.TOYOTA_RAV4_TSS2, -0.166)
|
|
||||||
samples = ((0.388, -0.201, -0.164), (0.330, -0.120, -0.140), (0.283, -0.0675, -0.120))
|
|
||||||
outputs = [control.update(True, make_car_state(v_ego, a_ego), a_target, True, (-3.5, 2.0)) for v_ego, a_ego, a_target in samples]
|
|
||||||
|
|
||||||
np.testing.assert_allclose(outputs, [-0.166] * len(samples), rtol=1e-6, atol=1e-12)
|
|
||||||
|
|
||||||
def test_glide_response_reaches_the_stock_rate_when_deceleration_stops(self):
|
|
||||||
_, control = make_control(TOYOTA.TOYOTA_RAV4_TSS2, -0.166)
|
|
||||||
control.update(True, make_car_state(0.388, -0.201), -0.164, True, (-3.5, 2.0))
|
|
||||||
output = control.update(True, make_car_state(0.330, -0.01), -0.140, True, (-3.5, 2.0))
|
|
||||||
|
|
||||||
assert -0.176 < output < -0.175
|
|
||||||
|
|
||||||
def test_glide_response_increases_with_stopping_distance_error(self):
|
|
||||||
_, nominal = make_control(TOYOTA.TOYOTA_RAV4_TSS2, -0.166)
|
|
||||||
_, distance_error = make_control(TOYOTA.TOYOTA_RAV4_TSS2, -0.166)
|
|
||||||
for control in (nominal, distance_error):
|
|
||||||
control.update(True, make_car_state(0.388, -0.201), -0.164, True, (-3.5, 2.0))
|
|
||||||
nominal_output = nominal.update(True, make_car_state(0.330, -0.050), -0.140, True, (-3.5, 2.0))
|
|
||||||
distance_error_output = distance_error.update(True, make_car_state(0.400, -0.050), -0.140, True, (-3.5, 2.0))
|
|
||||||
|
|
||||||
assert -0.176 < distance_error_output < nominal_output
|
|
||||||
|
|
||||||
@parameterized.expand(((1.0, 0.0), (0.75, 0.4375), (0.5, 0.75), (0.0, 1.0)), names=("decel_fraction", "expected_rate"))
|
|
||||||
def test_stopping_rate_scales_with_realized_deceleration(self, decel_fraction, expected_rate):
|
|
||||||
_, control = make_control(TOYOTA.TOYOTA_RAV4_TSS2)
|
|
||||||
output = control.update(True, make_car_state(0.3, -0.12 * decel_fraction), 0.0, True, (-3.5, 2.0))
|
|
||||||
|
|
||||||
self.assertAlmostEqual((-0.33 - output) / DT_CTRL, expected_rate, delta=1e-6)
|
|
||||||
|
|
||||||
def test_stopping_rate_scales_with_planner_demand(self):
|
|
||||||
_, gentle = make_control(TOYOTA.TOYOTA_RAV4_TSS2)
|
|
||||||
_, urgent = make_control(TOYOTA.TOYOTA_RAV4_TSS2)
|
|
||||||
|
|
||||||
gentle_output = gentle.update(True, make_car_state(0.3, -0.12), -0.34, True, (-3.5, 2.0))
|
|
||||||
urgent_output = urgent.update(True, make_car_state(0.3, -0.12), -1.0, True, (-3.5, 2.0))
|
|
||||||
|
|
||||||
assert -0.331 < gentle_output < -0.33
|
|
||||||
self.assertAlmostEqual(urgent_output, -0.34)
|
|
||||||
|
|
||||||
def test_glide_hold_yields_to_stronger_planner_braking(self):
|
|
||||||
CP, control = make_control(TOYOTA.TOYOTA_RAV4_TSS2, -0.166)
|
|
||||||
control.update(True, make_car_state(0.388, -0.201), -0.164, True, (-3.5, 2.0))
|
|
||||||
output = control.update(True, make_car_state(0.330, -0.120), -1.0, True, (-3.5, 2.0))
|
|
||||||
|
|
||||||
self.assertAlmostEqual(output, stock_stopping_output(-0.166, CP.stopAccel))
|
|
||||||
|
|
||||||
@parameterized.expand(STOP_ACCEL_VEHICLES, names=("candidate",))
|
|
||||||
def test_urgent_braking_matches_the_stock_ramp(self, candidate):
|
|
||||||
CP, control = make_control(candidate)
|
|
||||||
CS = make_car_state(0.8, -0.1)
|
|
||||||
output = control.last_output_accel
|
|
||||||
|
|
||||||
for _ in range(round(1.0 / DT_CTRL)):
|
|
||||||
output = control.update(True, CS, -3.0, True, (-3.5, 2.0))
|
|
||||||
|
|
||||||
expected = -0.33
|
|
||||||
for _ in range(round(1.0 / DT_CTRL)):
|
|
||||||
expected = stock_stopping_output(expected, CP.stopAccel)
|
|
||||||
self.assertAlmostEqual(output, expected)
|
|
||||||
|
|
||||||
@parameterized.expand(STOP_ACCEL_VEHICLES, names=("candidate",))
|
|
||||||
def test_stronger_planner_brake_matches_the_stock_ramp(self, candidate):
|
|
||||||
CP, control = make_control(candidate)
|
|
||||||
outputs = [control.update(True, make_car_state(0.3, -0.3), -1.0, True, (-3.5, 2.0)) for _ in range(10)]
|
|
||||||
expected = []
|
|
||||||
output = -0.33
|
|
||||||
for _ in range(10):
|
|
||||||
output = stock_stopping_output(output, CP.stopAccel)
|
|
||||||
expected.append(output)
|
|
||||||
np.testing.assert_allclose(outputs, expected, rtol=1e-6, atol=1e-12)
|
|
||||||
|
|
||||||
@parameterized.expand(STOP_ACCEL_VEHICLES, names=("candidate",))
|
|
||||||
def test_insufficient_deceleration_uses_most_of_the_stock_ramp(self, candidate):
|
|
||||||
CP, control = make_control(candidate)
|
|
||||||
output = control.update(True, make_car_state(0.6, -0.1), -0.1, True, (-3.5, 2.0))
|
|
||||||
if -0.33 > CP.stopAccel:
|
|
||||||
assert -0.34 < output < -0.338
|
|
||||||
else:
|
|
||||||
self.assertAlmostEqual(output, -0.33)
|
|
||||||
|
|
||||||
def test_deceleration_noise_cannot_release_the_brake(self):
|
|
||||||
_, control = make_control(TOYOTA.TOYOTA_RAV4_TSS2)
|
|
||||||
outputs = [control.update(True, make_car_state(0.3, -0.3 if frame % 2 else 0.0), -0.1, True, (-3.5, 2.0)) for frame in range(40)]
|
|
||||||
assert all(current <= previous for previous, current in zip(outputs[:-1], outputs[1:], strict=True))
|
|
||||||
|
|
||||||
def test_planner_noise_cannot_release_the_brake(self):
|
|
||||||
_, control = make_control(TOYOTA.TOYOTA_RAV4_TSS2)
|
|
||||||
outputs = [control.update(True, make_car_state(0.3, -0.3), -1.0 if frame % 2 else -0.1, True, (-3.5, 2.0)) for frame in range(40)]
|
|
||||||
assert all(current <= previous for previous, current in zip(outputs[:-1], outputs[1:], strict=True))
|
|
||||||
|
|
||||||
@parameterized.expand(
|
|
||||||
(
|
|
||||||
(float("nan"), -0.3, -0.1),
|
|
||||||
(0.3, float("nan"), -0.1),
|
|
||||||
(0.3, -0.3, float("nan")),
|
|
||||||
(float("inf"), -0.3, -0.1),
|
|
||||||
(0.3, -float("inf"), -0.1),
|
|
||||||
(0.3, -0.3, float("inf")),
|
|
||||||
),
|
|
||||||
names=("v_ego", "a_ego", "a_target"),
|
|
||||||
)
|
|
||||||
def test_invalid_state_uses_the_stock_ramp(self, v_ego, a_ego, a_target):
|
|
||||||
CP, control = make_control(TOYOTA.TOYOTA_RAV4_TSS2)
|
|
||||||
output = control.update(True, make_car_state(v_ego, a_ego), a_target, True, (-3.5, 2.0))
|
|
||||||
self.assertAlmostEqual(output, stock_stopping_output(-0.33, CP.stopAccel))
|
|
||||||
|
|
||||||
@parameterized.expand(
|
|
||||||
(
|
|
||||||
(0.24, 0.0, -0.49, 0.15, 0.0),
|
|
||||||
(0.53, -0.31, -0.49, 0.35, 0.1),
|
|
||||||
(0.24, 0.0, 0.0, 0.15, 0.0),
|
|
||||||
(0.464, -0.223, 0.0, 0.25, 0.05),
|
|
||||||
(0.53, -0.31, 0.0, 0.35, 0.1),
|
|
||||||
(0.24, 0.0, 0.49, 0.15, 0.0),
|
|
||||||
(0.53, -0.31, 0.49, 0.25, 0.05),
|
|
||||||
(0.6, -0.3, 0.49, 0.35, 0.1),
|
|
||||||
(0.6, -0.3, 0.49, 0.5, 0.1),
|
|
||||||
),
|
|
||||||
names=("speed", "initial_accel", "grade_accel", "actuator_lag", "actuator_delay"),
|
|
||||||
)
|
|
||||||
def test_smooth_stop_distance_is_bounded(self, speed, initial_accel, grade_accel, actuator_lag, actuator_delay):
|
|
||||||
_, control = make_control(TOYOTA.TOYOTA_RAV4_TSS2, initial_accel)
|
|
||||||
applied_accel = initial_accel
|
|
||||||
delay = [initial_accel] * round(actuator_delay / DT_CTRL)
|
|
||||||
distance = 0.0
|
|
||||||
outputs = []
|
|
||||||
|
|
||||||
for _ in range(round(4.0 / DT_CTRL)):
|
|
||||||
command = control.update(True, make_car_state(speed, applied_accel), -0.1, True, (-3.5, 2.0))
|
|
||||||
outputs.append(command)
|
|
||||||
delayed_command = command
|
|
||||||
if delay:
|
|
||||||
delay.append(command)
|
|
||||||
delayed_command = delay.pop(0)
|
|
||||||
applied_accel += DT_CTRL / actuator_lag * (delayed_command + grade_accel - applied_accel)
|
|
||||||
speed = max(0.0, speed + applied_accel * DT_CTRL)
|
|
||||||
distance += speed * DT_CTRL
|
|
||||||
if speed == 0.0:
|
|
||||||
break
|
|
||||||
|
|
||||||
assert speed == 0.0
|
|
||||||
assert distance < 1.0
|
|
||||||
assert all(current <= previous for previous, current in zip(outputs[:-1], outputs[1:], strict=True))
|
|
||||||
|
|
||||||
@parameterized.expand(STOP_ACCEL_VEHICLES, names=("candidate",))
|
|
||||||
def test_standstill_uses_the_stock_ramp(self, candidate):
|
|
||||||
CP, control = make_control(candidate)
|
|
||||||
control.long_control_state = LongCtrlState.off
|
|
||||||
CS = make_car_state(0.0, 0.0, standstill=True)
|
|
||||||
outputs = [control.update(True, CS, 0.0, False, (-3.5, 2.0)) for _ in range(round(2.0 / DT_CTRL))]
|
|
||||||
expected = -0.33
|
|
||||||
for _ in range(round(2.0 / DT_CTRL)):
|
|
||||||
expected = stock_stopping_output(expected, CP.stopAccel)
|
|
||||||
self.assertAlmostEqual(outputs[0], stock_stopping_output(-0.33, CP.stopAccel))
|
|
||||||
self.assertAlmostEqual(outputs[-1], expected)
|
|
||||||
|
|
||||||
@parameterized.expand(PRESERVED_HOLD_VEHICLES, names=("candidate",))
|
|
||||||
def test_preserved_hold_yields_to_stronger_planner_braking(self, candidate):
|
|
||||||
CP, control = make_control(candidate)
|
|
||||||
settle_preserved_hold(control)
|
|
||||||
previous = control.last_output_accel
|
|
||||||
output = control.update(True, make_car_state(0.0, 0.0, standstill=True), CP.stopAccel, True, (-3.5, 2.0))
|
|
||||||
|
|
||||||
self.assertAlmostEqual(output, stock_stopping_output(previous, CP.stopAccel))
|
|
||||||
|
|
||||||
def test_false_departure_restores_a_stronger_preserved_hold(self):
|
|
||||||
CP, control = make_control(TOYOTA.TOYOTA_RAV4_TSS2)
|
|
||||||
settle_preserved_hold(control)
|
|
||||||
stronger_hold = control.update(True, make_car_state(0.0, 0.0, standstill=True), CP.stopAccel, True, (-3.5, 2.0))
|
|
||||||
departure_state = make_car_state(0.0, 0.0, standstill=True)
|
|
||||||
departure_state.cruiseState.standstill = False
|
|
||||||
control.update(True, departure_state, 0.6, False, (-3.5, 2.0))
|
|
||||||
restored = control.update(True, make_car_state(0.0, 0.0, standstill=True), -0.1, True, (-3.5, 2.0))
|
|
||||||
|
|
||||||
self.assertAlmostEqual(restored, stronger_hold)
|
|
||||||
|
|
||||||
@parameterized.expand(SETTLE_VEHICLES, names=("candidate",))
|
|
||||||
def test_false_departure_restores_a_command_at_the_stop_limit(self, candidate):
|
|
||||||
CP, control = make_control(candidate)
|
|
||||||
strong_hold = max(CP.stopAccel - 0.2, -3.5)
|
|
||||||
control.last_output_accel = strong_hold
|
|
||||||
held = control.update(True, make_car_state(0.0, 0.0, standstill=True), -0.1, True, (-3.5, 2.0))
|
|
||||||
departure_state = make_car_state(0.0, 0.0, standstill=True)
|
|
||||||
departure_state.cruiseState.standstill = False
|
|
||||||
control.update(True, departure_state, 0.6, False, (-3.5, 2.0))
|
|
||||||
restored = control.update(True, make_car_state(0.0, 0.0, standstill=True), -0.1, True, (-3.5, 2.0))
|
|
||||||
|
|
||||||
self.assertAlmostEqual(restored, held)
|
|
||||||
|
|
||||||
@parameterized.expand(SETTLE_VEHICLES, names=("candidate",))
|
|
||||||
def test_false_departure_after_reaching_the_stop_limit_restores_braking(self, candidate):
|
|
||||||
CP, control = make_control(candidate)
|
|
||||||
CS = make_car_state(0.0, 0.0, standstill=True)
|
|
||||||
while control.last_output_accel > CP.stopAccel:
|
|
||||||
reached = control.update(True, CS, -0.1, True, (-3.5, 2.0))
|
|
||||||
|
|
||||||
departure_state = make_car_state(0.0, 0.0, standstill=True)
|
|
||||||
departure_state.cruiseState.standstill = False
|
|
||||||
control.update(True, departure_state, 0.6, False, (-3.5, 2.0))
|
|
||||||
restored = control.update(True, CS, -0.1, True, (-3.5, 2.0))
|
|
||||||
|
|
||||||
self.assertAlmostEqual(restored, reached)
|
|
||||||
|
|
||||||
@parameterized.expand(PRESERVED_HOLD_VEHICLES, names=("candidate",))
|
|
||||||
def test_inadequate_preserved_hold_uses_the_stock_ramp(self, candidate):
|
|
||||||
for v_ego, a_ego, standstill in ((0.0, 0.2, True), (-0.1, 0.0, False)):
|
|
||||||
with self.subTest(v_ego=v_ego, a_ego=a_ego, standstill=standstill):
|
|
||||||
CP, control = make_control(candidate)
|
|
||||||
settle_preserved_hold(control)
|
|
||||||
output = control.last_output_accel
|
|
||||||
expected = output
|
|
||||||
for _ in range(round(4.0 / DT_CTRL)):
|
|
||||||
output = control.update(True, make_car_state(v_ego, a_ego, standstill), -0.1, True, (-3.5, 2.0))
|
|
||||||
expected = max(stock_stopping_output(expected, CP.stopAccel), -3.5)
|
|
||||||
|
|
||||||
self.assertAlmostEqual(output, expected)
|
|
||||||
|
|
||||||
@parameterized.expand(PRESERVED_HOLD_VEHICLES, names=("candidate",))
|
|
||||||
def test_false_departure_restores_the_preserved_hold(self, candidate):
|
|
||||||
_, control = make_control(candidate)
|
|
||||||
settle_preserved_hold(control)
|
|
||||||
hold_accel = control.last_output_accel
|
|
||||||
departure_state = make_car_state(0.0, 0.0, standstill=True)
|
|
||||||
departure_state.cruiseState.standstill = False
|
|
||||||
departure = control.update(True, departure_state, 0.6, False, (-3.5, 2.0))
|
|
||||||
restored = control.update(True, make_car_state(0.0, 0.0, standstill=True), -0.1, True, (-3.5, 2.0))
|
|
||||||
|
|
||||||
assert departure > 0.0
|
|
||||||
self.assertAlmostEqual(restored, hold_accel)
|
|
||||||
|
|
||||||
@parameterized.expand(
|
|
||||||
((True, 0.0, 0.0, True), (False, 0.06, 0.06, False), (False, 0.0, 0.06, False)),
|
|
||||||
names=("inactive", "v_ego", "v_ego_raw", "standstill"),
|
|
||||||
)
|
|
||||||
def test_preserved_hold_clears_after_inactive_or_real_motion(self, inactive, v_ego, v_ego_raw, standstill):
|
|
||||||
CP, control = make_control(TOYOTA.TOYOTA_RAV4_TSS2)
|
|
||||||
settle_preserved_hold(control)
|
|
||||||
control.update(not inactive, make_car_state(v_ego, 0.0, standstill=standstill, v_ego_raw=v_ego_raw), 0.6, False, (-3.5, 2.0))
|
|
||||||
output = control.update(True, make_car_state(0.0, 0.0, standstill=True), -0.1, True, (-3.5, 2.0))
|
|
||||||
|
|
||||||
assert control._stopping_hold_accel is None
|
|
||||||
self.assertAlmostEqual(output, stock_stopping_output(0.0, CP.stopAccel))
|
|
||||||
|
|
||||||
@parameterized.expand(((float("nan"), 0.0), (0.0, float("nan"))), names=("v_ego", "v_ego_raw"))
|
|
||||||
def test_invalid_speed_clears_the_preserved_hold(self, v_ego, v_ego_raw):
|
|
||||||
_, control = make_control(TOYOTA.TOYOTA_RAV4_TSS2)
|
|
||||||
settle_preserved_hold(control)
|
|
||||||
previous = control.last_output_accel
|
|
||||||
output = control.update(True, make_car_state(v_ego, 0.0, standstill=True, v_ego_raw=v_ego_raw), -0.1, True, (-3.5, 2.0))
|
|
||||||
|
|
||||||
assert control._stopping_hold_accel is None
|
|
||||||
self.assertAlmostEqual(output, previous - DT_CTRL)
|
|
||||||
|
|
||||||
@parameterized.expand(((0.005, True), (-0.005, True), (0.02, True), (-0.02, False)), names=("v_ego_raw", "standstill"))
|
|
||||||
def test_raw_wheel_motion_keeps_building_brake(self, v_ego_raw, standstill):
|
|
||||||
_, control = make_control(TOYOTA.TOYOTA_RAV4_TSS2)
|
|
||||||
settle_preserved_hold(control)
|
|
||||||
previous = control.last_output_accel
|
|
||||||
CS = make_car_state(0.0, 0.0, standstill=standstill, v_ego_raw=v_ego_raw)
|
|
||||||
output = control.update(True, CS, -0.1, True, (-3.5, 2.0))
|
|
||||||
|
|
||||||
self.assertAlmostEqual(output, previous - DT_CTRL)
|
|
||||||
|
|
||||||
def test_preserved_hold_removes_launch_brake_backlog(self):
|
|
||||||
CP, control = make_control(TOYOTA.TOYOTA_RAV4_TSS2)
|
|
||||||
control.update(True, make_car_state(0.28, -0.29), -0.22, True, (-3.5, 2.0))
|
|
||||||
CS = make_car_state(0.0, 0.0, standstill=True)
|
|
||||||
for _ in range(round(3.0 / DT_CTRL)):
|
|
||||||
control.update(True, CS, -0.1, True, (-3.5, 2.0))
|
|
||||||
preserved_hold = control.last_output_accel
|
|
||||||
|
|
||||||
CS.cruiseState.standstill = False
|
|
||||||
requested_accels = [control.update(True, CS, min(0.15 + frame * DT_CTRL, 1.2), False, (-3.5, 2.0)) for frame in range(round(1.0 / DT_CTRL))]
|
|
||||||
|
|
||||||
def release_time(initial_accel):
|
|
||||||
applied_accel = initial_accel
|
|
||||||
for frame, requested_accel in enumerate(requested_accels):
|
|
||||||
accel_step = PRIUS_TSS2_ROUTE_MODEL.command_rate_limit * DT_CTRL
|
|
||||||
applied_accel += np.clip(requested_accel - applied_accel, -accel_step, accel_step)
|
|
||||||
if applied_accel >= 0.0:
|
|
||||||
return (frame + 1) * DT_CTRL
|
|
||||||
raise AssertionError("brake command did not release")
|
|
||||||
|
|
||||||
stock_release = release_time(CP.stopAccel)
|
|
||||||
preserved_release = release_time(preserved_hold)
|
|
||||||
self.assertAlmostEqual(preserved_hold, expected_hold_accel(CP))
|
|
||||||
assert stock_release >= 0.45
|
|
||||||
assert preserved_release <= 0.37
|
|
||||||
assert stock_release - preserved_release >= 0.14
|
|
||||||
|
|
||||||
@parameterized.expand(GRADE_HOLD_CASES, names=("grade_accel", "expected_hold"))
|
|
||||||
def test_preserved_hold_adapts_to_grade_without_creep(self, grade_accel, expected_hold):
|
|
||||||
_, control = make_control(TOYOTA.TOYOTA_RAV4_TSS2, -0.3)
|
|
||||||
speed = 0.6
|
|
||||||
actuator_accel = -0.3
|
|
||||||
physical_accel = actuator_accel + grade_accel
|
|
||||||
stopped_frames = 0
|
|
||||||
max_post_stop_speed = 0.0
|
|
||||||
|
|
||||||
for _ in range(round(16.0 / DT_CTRL)):
|
|
||||||
standstill = bool(speed <= 1e-6)
|
|
||||||
measured_accel = max(physical_accel, 0.0) if standstill else physical_accel
|
|
||||||
output = control.update(True, make_car_state(speed, measured_accel, standstill), -0.1, True, (-3.5, 2.0))
|
|
||||||
actuator_accel += DT_CTRL / 0.25 * (output - actuator_accel)
|
|
||||||
physical_accel = actuator_accel + grade_accel
|
|
||||||
speed = max(0.0, speed + physical_accel * DT_CTRL) if speed > 0.0 or physical_accel > 0.0 else 0.0
|
|
||||||
|
|
||||||
if stopped_frames:
|
|
||||||
max_post_stop_speed = max(max_post_stop_speed, speed)
|
|
||||||
stopped_frames += 1
|
|
||||||
elif speed == 0.0:
|
|
||||||
stopped_frames = 1
|
|
||||||
if stopped_frames >= round(8.0 / DT_CTRL):
|
|
||||||
break
|
|
||||||
|
|
||||||
assert stopped_frames >= round(8.0 / DT_CTRL)
|
|
||||||
assert max_post_stop_speed == 0.0
|
|
||||||
self.assertAlmostEqual(output, expected_hold, delta=0.03)
|
|
||||||
|
|
||||||
@parameterized.expand(SETTLE_VEHICLES, names=("candidate",))
|
|
||||||
def test_final_stop_builds_brake_smoothly_while_vehicle_settles(self, candidate):
|
|
||||||
_, control = make_control(candidate)
|
|
||||||
control.update(True, make_car_state(0.28, -0.29), -0.22, True, (-3.5, 2.0))
|
|
||||||
outputs = [control.update(True, make_car_state(0.0006, a_ego, standstill=True), -0.032, True, (-3.5, 2.0)) for a_ego in (-1.098, -0.950, -0.609, -0.286)]
|
|
||||||
changes = -np.diff([-0.33, *outputs])
|
|
||||||
assert np.all(changes > 0.0)
|
|
||||||
assert np.all(np.diff(changes) > 0.0)
|
|
||||||
assert changes[-1] < 0.001
|
|
||||||
|
|
||||||
@parameterized.expand((-0.09, 0.0, 0.1), names=("a_ego",))
|
|
||||||
def test_settled_vehicle_uses_the_stock_hold_ramp(self, a_ego):
|
|
||||||
CP, control = make_control(TOYOTA.TOYOTA_RAV4_TSS2)
|
|
||||||
output = control.update(True, make_car_state(0.0, a_ego, standstill=True), -0.1, True, (-3.5, 2.0))
|
|
||||||
self.assertAlmostEqual(output, stock_stopping_output(-0.33, CP.stopAccel))
|
|
||||||
|
|
||||||
@parameterized.expand(SETTLE_VEHICLES, names=("candidate",))
|
|
||||||
def test_direct_terminal_entry_builds_brake_smoothly(self, candidate):
|
|
||||||
_, control = make_control(candidate)
|
|
||||||
CS = make_car_state(0.0006, -0.3, standstill=True)
|
|
||||||
outputs = [control.update(True, CS, -0.1, True, (-3.5, 2.0)) for _ in range(4)]
|
|
||||||
|
|
||||||
rates = -np.diff([-0.33, *outputs]) / DT_CTRL
|
|
||||||
np.testing.assert_allclose(rates, [(frame / STOPPING_SETTLE_FRAMES) ** 2 for frame in range(1, 5)], rtol=1e-6, atol=1e-12)
|
|
||||||
|
|
||||||
def test_direct_terminal_entry_keeps_urgent_stock_braking(self):
|
|
||||||
CP, control = make_control(TOYOTA.TOYOTA_RAV4_TSS2)
|
|
||||||
output = control.update(True, make_car_state(0.0006, -0.3, standstill=True), -1.0, True, (-3.5, 2.0))
|
|
||||||
|
|
||||||
self.assertAlmostEqual(output, stock_stopping_output(-0.33, CP.stopAccel))
|
|
||||||
|
|
||||||
@parameterized.expand((0.0, -0.05), names=("initial_accel",))
|
|
||||||
def test_direct_terminal_entry_first_builds_meaningful_brake(self, initial_accel):
|
|
||||||
CP, control = make_control(TOYOTA.TOYOTA_RAV4_TSS2, initial_accel)
|
|
||||||
output = control.update(True, make_car_state(0.0006, -0.3, standstill=True), 0.0, True, (-3.5, 2.0))
|
|
||||||
|
|
||||||
self.assertAlmostEqual(output, stock_stopping_output(initial_accel, CP.stopAccel))
|
|
||||||
|
|
||||||
@parameterized.expand(SETTLE_VEHICLES, names=("candidate",))
|
|
||||||
def test_final_settling_ramp_is_bounded(self, candidate):
|
|
||||||
_, control = make_control(candidate)
|
|
||||||
control.update(True, make_car_state(0.28, -0.29), -0.22, True, (-3.5, 2.0))
|
|
||||||
CS = make_car_state(0.0, -0.3, standstill=True)
|
|
||||||
outputs = [control.update(True, CS, -0.1, True, (-3.5, 2.0)) for _ in range(STOPPING_SETTLE_FRAMES + 1)]
|
|
||||||
|
|
||||||
rates = -np.diff([-0.33, *outputs]) / DT_CTRL
|
|
||||||
expected = [(frame / STOPPING_SETTLE_FRAMES) ** 2 for frame in range(1, STOPPING_SETTLE_FRAMES + 1)] + [1.0]
|
|
||||||
np.testing.assert_allclose(rates, expected, rtol=1e-6, atol=1e-12)
|
|
||||||
|
|
||||||
@parameterized.expand(((0.6, -0.1, False), (0.0, 0.0, True)), names=("v_ego", "a_ego", "standstill"))
|
|
||||||
def test_stopping_never_releases_a_stronger_command(self, v_ego, a_ego, standstill):
|
|
||||||
_, control = make_control(TOYOTA.TOYOTA_RAV4_TSS2, -3.0)
|
|
||||||
output = control.update(True, make_car_state(v_ego, a_ego, standstill), 0.0, True, (-3.5, 2.0))
|
|
||||||
self.assertAlmostEqual(output, -3.0)
|
|
||||||
|
|
||||||
def test_reported_standstill_while_moving_can_hold_the_brake(self):
|
|
||||||
_, control = make_control(GM.CHEVROLET_BOLT_EUV)
|
|
||||||
control.long_control_state = LongCtrlState.off
|
|
||||||
output = control.update(True, make_car_state(0.3, -0.3, standstill=True), -0.1, False, (-3.5, 2.0))
|
|
||||||
self.assertAlmostEqual(output, -0.33)
|
|
||||||
|
|
||||||
@parameterized.expand((-0.1, 0.09), names=("a_target",))
|
|
||||||
def test_stopping_removes_positive_acceleration_immediately(self, a_target):
|
|
||||||
_, control = make_control(HYUNDAI.HYUNDAI_SONATA, 0.2)
|
|
||||||
output = control.update(True, make_car_state(0.2, -0.2), a_target, True, (-3.5, 2.0))
|
|
||||||
self.assertAlmostEqual(output, -DT_CTRL)
|
|
||||||
|
|
||||||
def test_rollback_uses_the_stock_ramp(self):
|
|
||||||
CP, control = make_control(TOYOTA.TOYOTA_RAV4_TSS2)
|
|
||||||
output = control.update(True, make_car_state(-0.1, 0.1), -0.1, True, (-3.5, 2.0))
|
|
||||||
self.assertAlmostEqual(output, stock_stopping_output(-0.33, CP.stopAccel))
|
|
||||||
|
|
||||||
def test_rollback_after_settling_arms_uses_the_stock_ramp(self):
|
|
||||||
_, control = make_control(TOYOTA.TOYOTA_RAV4_TSS2)
|
|
||||||
control.update(True, make_car_state(0.28, -0.29), -0.22, True, (-3.5, 2.0))
|
|
||||||
control.update(True, make_car_state(0.01, -0.3), -0.1, True, (-3.5, 2.0))
|
|
||||||
previous = control.last_output_accel
|
|
||||||
output = control.update(True, make_car_state(-0.04, -0.3), -0.1, True, (-3.5, 2.0))
|
|
||||||
|
|
||||||
self.assertAlmostEqual(output, previous - DT_CTRL)
|
|
||||||
|
|
||||||
def test_small_velocity_noise_does_not_trigger_the_stock_rate(self):
|
|
||||||
_, control = make_control(TOYOTA.TOYOTA_RAV4_TSS2)
|
|
||||||
control.update(True, make_car_state(0.28, -0.29), -0.22, True, (-3.5, 2.0))
|
|
||||||
output = control.update(True, make_car_state(-0.04, -0.3, standstill=True, v_ego_raw=0.0), -0.1, True, (-3.5, 2.0))
|
|
||||||
assert -0.331 < output < -0.33
|
|
||||||
|
|
||||||
def test_terminal_speed_chatter_cannot_extend_settling_ramp(self):
|
|
||||||
_, control = make_control(TOYOTA.TOYOTA_RAV4_TSS2)
|
|
||||||
control.update(True, make_car_state(0.28, -0.29), -0.22, True, (-3.5, 2.0))
|
|
||||||
outputs = [
|
|
||||||
control.update(True, make_car_state(0.049 if frame % 2 == 0 else 0.051, -0.3, v_ego_raw=0.0), -0.1, True, (-3.5, 2.0))
|
|
||||||
for frame in range(STOPPING_SETTLE_FRAMES + 2)
|
|
||||||
]
|
|
||||||
|
|
||||||
rates = -np.diff([-0.33, *outputs]) / DT_CTRL
|
|
||||||
np.testing.assert_allclose(
|
|
||||||
rates[:STOPPING_SETTLE_FRAMES], [(frame / STOPPING_SETTLE_FRAMES) ** 2 for frame in range(1, STOPPING_SETTLE_FRAMES + 1)], rtol=1e-6, atol=1e-12
|
|
||||||
)
|
|
||||||
np.testing.assert_allclose(rates[-2:], [1.0, 1.0], rtol=1e-6, atol=1e-12)
|
|
||||||
|
|
||||||
def test_terminal_speed_plateau_cannot_extend_settling_ramp(self):
|
|
||||||
_, control = make_control(TOYOTA.TOYOTA_RAV4_TSS2)
|
|
||||||
control.update(True, make_car_state(0.28, -0.29), -0.22, True, (-3.5, 2.0))
|
|
||||||
CS = make_car_state(0.03, -0.3, v_ego_raw=0.0)
|
|
||||||
outputs = [control.update(True, CS, -0.1, True, (-3.5, 2.0)) for _ in range(STOPPING_SETTLE_FRAMES + 1)]
|
|
||||||
|
|
||||||
rates = -np.diff([-0.33, *outputs]) / DT_CTRL
|
|
||||||
np.testing.assert_allclose(rates[-2:], [1.0, 1.0], rtol=1e-6, atol=1e-12)
|
|
||||||
|
|
||||||
def test_interrupted_stop_cannot_reuse_settling_hold(self):
|
|
||||||
CP, control = make_control(TOYOTA.TOYOTA_RAV4_TSS2)
|
|
||||||
control.update(True, make_car_state(0.28, -0.29), -0.22, True, (-3.5, 2.0))
|
|
||||||
control.update(False, make_car_state(0.0, 0.0, standstill=True), 0.0, False, (-3.5, 2.0))
|
|
||||||
output = control.update(True, make_car_state(0.0, -0.3, standstill=True), -0.1, True, (-3.5, 2.0))
|
|
||||||
|
|
||||||
self.assertAlmostEqual(output, stock_stopping_output(0.0, CP.stopAccel))
|
|
||||||
|
|
||||||
def test_departure_uses_the_stock_pid_path(self):
|
|
||||||
_, control = make_control(TOYOTA.TOYOTA_RAV4_TSS2)
|
|
||||||
control.long_control_state = LongCtrlState.stopping
|
|
||||||
output = control.update(True, make_car_state(0.0), 0.6, False, (-3.5, 2.0))
|
|
||||||
assert control.long_control_state == LongCtrlState.pid
|
|
||||||
assert output > 0.0
|
|
||||||
|
|
||||||
def test_planner_mpc_and_longcontrol_complete_a_smooth_stop(self):
|
|
||||||
plant = PlantSP(
|
|
||||||
lead_relevancy=True,
|
|
||||||
speed=0.6,
|
|
||||||
distance_lead=3.6,
|
|
||||||
run_long_control=True,
|
|
||||||
actuator_model=PRIUS_TSS2_ROUTE_MODEL,
|
|
||||||
)
|
|
||||||
plant.planner.accel_controller._enabled = True
|
|
||||||
plant.planner.dec._enabled = False
|
|
||||||
commands = []
|
|
||||||
speeds = []
|
|
||||||
states = []
|
|
||||||
solver_statuses = []
|
|
||||||
|
|
||||||
with (
|
|
||||||
mock.patch.object(plant.planner.accel_controller, "update", return_value=None),
|
|
||||||
mock.patch.object(plant.planner.dec, "_read_params", return_value=None),
|
|
||||||
):
|
|
||||||
while plant.current_time < 5.0:
|
|
||||||
result = plant.step(v_lead=0.0, v_cruise=8.0)
|
|
||||||
commands.append(result["actuator_command"])
|
|
||||||
speeds.append(result["speed"])
|
|
||||||
states.append(result["long_control_state"])
|
|
||||||
solver_statuses.append(plant.planner.mpc.solution_status)
|
|
||||||
|
|
||||||
stopping = states.index(LongCtrlState.stopping)
|
|
||||||
moving_stop_commands = [
|
|
||||||
command for command, state, speed in zip(commands, states, speeds, strict=True) if state == LongCtrlState.stopping and speed > STOPPING_SPEED_TOLERANCE
|
|
||||||
]
|
|
||||||
assert all(current <= previous + 1e-9 for previous, current in zip(commands[stopping:-1], commands[stopping + 1 :], strict=True))
|
|
||||||
assert len(moving_stop_commands) > 1 and max(moving_stop_commands) - min(moving_stop_commands) < 1e-9
|
|
||||||
assert plant.speed == 0.0 and plant.distance < 1.0
|
|
||||||
assert plant.distance_lead - plant.distance > 3.0
|
|
||||||
assert all(status == 0 for status in solver_statuses)
|
|
||||||
@@ -1,433 +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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
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),
|
|
||||||
}
|
|
||||||
-179
@@ -1,179 +0,0 @@
|
|||||||
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(-2.5),
|
|
||||||
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_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)
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
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,53 +652,6 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"key": "AccelPersonalityEnabled",
|
|
||||||
"widget": "toggle",
|
|
||||||
"title": "Enable Accel Controller",
|
|
||||||
"description": "Sets your preferred acceleration and cruise-deceleration limits by profile. Lead following, braking, and stopping behavior remain independent of this setting.",
|
|
||||||
"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": "Select the vehicle acceleration response. Chauffeur braking and stopping behavior remain the same across profiles.",
|
|
||||||
"options": [
|
|
||||||
{
|
|
||||||
"value": 0,
|
|
||||||
"label": "Eco"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": 1,
|
|
||||||
"label": "Normal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": 2,
|
|
||||||
"label": "Sport"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"enablement": [
|
|
||||||
{
|
|
||||||
"type": "capability",
|
|
||||||
"field": "has_longitudinal_control",
|
|
||||||
"equals": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"key": "IntelligentCruiseButtonManagement",
|
"key": "IntelligentCruiseButtonManagement",
|
||||||
"widget": "toggle",
|
"widget": "toggle",
|
||||||
@@ -2349,50 +2302,6 @@
|
|||||||
"title": "Toyota / Lexus Settings",
|
"title": "Toyota / Lexus Settings",
|
||||||
"description": "",
|
"description": "",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
|
||||||
"key": "ToyotaAutoHold",
|
|
||||||
"widget": "toggle",
|
|
||||||
"needs_onroad_cycle": true,
|
|
||||||
"title": "Toyota: Auto Brake Hold FOR TSS2 HYBRID CARS",
|
|
||||||
"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",
|
"key": "ToyotaEnforceStockLongitudinal",
|
||||||
"widget": "toggle",
|
"widget": "toggle",
|
||||||
|
|||||||
@@ -43,29 +43,6 @@ sections:
|
|||||||
label: Relaxed
|
label: Relaxed
|
||||||
enablement:
|
enablement:
|
||||||
- $ref: '#/macros/longitudinal'
|
- $ref: '#/macros/longitudinal'
|
||||||
- key: AccelPersonalityEnabled
|
|
||||||
widget: toggle
|
|
||||||
title: Enable Accel Controller
|
|
||||||
description: Sets your preferred acceleration and cruise-deceleration limits by profile. Lead following, braking,
|
|
||||||
and stopping behavior remain independent of this setting.
|
|
||||||
visibility:
|
|
||||||
- $ref: '#/macros/longitudinal'
|
|
||||||
enablement:
|
|
||||||
- $ref: '#/macros/longitudinal'
|
|
||||||
- key: AccelPersonality
|
|
||||||
widget: multiple_button
|
|
||||||
title: Acceleration Profile
|
|
||||||
description: Select the vehicle acceleration response. Chauffeur braking and stopping behavior remain the same across
|
|
||||||
profiles.
|
|
||||||
options:
|
|
||||||
- value: 0
|
|
||||||
label: Eco
|
|
||||||
- value: 1
|
|
||||||
label: Normal
|
|
||||||
- value: 2
|
|
||||||
label: Sport
|
|
||||||
enablement:
|
|
||||||
- $ref: '#/macros/longitudinal'
|
|
||||||
- key: IntelligentCruiseButtonManagement
|
- key: IntelligentCruiseButtonManagement
|
||||||
widget: toggle
|
widget: toggle
|
||||||
title: Intelligent Cruise Button Management (ICBM) (Alpha)
|
title: Intelligent Cruise Button Management (ICBM) (Alpha)
|
||||||
|
|||||||
@@ -82,30 +82,6 @@ sections:
|
|||||||
title: Toyota / Lexus Settings
|
title: Toyota / Lexus Settings
|
||||||
description: ''
|
description: ''
|
||||||
items:
|
items:
|
||||||
- key: ToyotaAutoHold
|
|
||||||
widget: toggle
|
|
||||||
needs_onroad_cycle: true
|
|
||||||
title: 'Toyota: Auto Brake Hold FOR TSS2 HYBRID CARS'
|
|
||||||
enablement:
|
|
||||||
- $ref: '#/macros/not_engaged'
|
|
||||||
- key: ToyotaEnhancedBsm
|
|
||||||
widget: toggle
|
|
||||||
needs_onroad_cycle: true
|
|
||||||
title: 'Toyota: Prius TSS2 BSM and some tssp'
|
|
||||||
enablement:
|
|
||||||
- $ref: '#/macros/not_engaged'
|
|
||||||
- key: ToyotaTSS2Long
|
|
||||||
widget: toggle
|
|
||||||
needs_onroad_cycle: true
|
|
||||||
title: 'Toyota: custom longitudinal for TSS2'
|
|
||||||
enablement:
|
|
||||||
- $ref: '#/macros/not_engaged'
|
|
||||||
- key: ToyotaDriveMode
|
|
||||||
widget: toggle
|
|
||||||
needs_onroad_cycle: true
|
|
||||||
title: Enable drive mode btn link
|
|
||||||
enablement:
|
|
||||||
- $ref: '#/macros/not_engaged'
|
|
||||||
- key: ToyotaEnforceStockLongitudinal
|
- key: ToyotaEnforceStockLongitudinal
|
||||||
widget: toggle
|
widget: toggle
|
||||||
needs_onroad_cycle: true
|
needs_onroad_cycle: true
|
||||||
|
|||||||
@@ -10,10 +10,9 @@ change and must be intentional. KNOWN_PROTOCOL_VERSIONS pins the set we
|
|||||||
explicitly support — when the constant is bumped, this list must be edited in
|
explicitly support — when the constant is bumped, this list must be edited in
|
||||||
the same commit so the bump shows up in code review.
|
the same commit so the bump shows up in code review.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from openpilot.common.test import OpenpilotTestCase
|
|
||||||
from openpilot.sunnypilot.sunnylink.capabilities import (
|
from openpilot.sunnypilot.sunnylink.capabilities import (
|
||||||
CAPABILITY_DEFAULTS,
|
CAPABILITY_DEFAULTS,
|
||||||
CAPABILITY_FIELDS,
|
CAPABILITY_FIELDS,
|
||||||
@@ -21,23 +20,13 @@ from openpilot.sunnypilot.sunnylink.capabilities import (
|
|||||||
PROTOCOL_VERSION,
|
PROTOCOL_VERSION,
|
||||||
generate_capabilities,
|
generate_capabilities,
|
||||||
)
|
)
|
||||||
|
from openpilot.common.test import OpenpilotTestCase
|
||||||
|
|
||||||
|
|
||||||
KNOWN_PROTOCOL_VERSIONS = (1,)
|
KNOWN_PROTOCOL_VERSIONS = (1,)
|
||||||
LATEST_KNOWN = max(KNOWN_PROTOCOL_VERSIONS)
|
LATEST_KNOWN = max(KNOWN_PROTOCOL_VERSIONS)
|
||||||
|
|
||||||
|
|
||||||
class FakeParams:
|
|
||||||
def __init__(self, values=None):
|
|
||||||
self.values = values or {}
|
|
||||||
|
|
||||||
def get(self, key, *args, **kwargs):
|
|
||||||
return self.values.get(key)
|
|
||||||
|
|
||||||
def get_bool(self, key):
|
|
||||||
return bool(self.values.get(key, False))
|
|
||||||
|
|
||||||
|
|
||||||
def caps():
|
def caps():
|
||||||
return generate_capabilities()
|
return generate_capabilities()
|
||||||
|
|
||||||
@@ -63,12 +52,14 @@ class TestProtocolVersion(OpenpilotTestCase):
|
|||||||
def test_protocol_version_is_known(self):
|
def test_protocol_version_is_known(self):
|
||||||
"""Sentinel against accidental bumps. Edit KNOWN_PROTOCOL_VERSIONS if intentional."""
|
"""Sentinel against accidental bumps. Edit KNOWN_PROTOCOL_VERSIONS if intentional."""
|
||||||
assert PROTOCOL_VERSION in KNOWN_PROTOCOL_VERSIONS, (
|
assert PROTOCOL_VERSION in KNOWN_PROTOCOL_VERSIONS, (
|
||||||
f"PROTOCOL_VERSION={PROTOCOL_VERSION} is not in KNOWN_PROTOCOL_VERSIONS={KNOWN_PROTOCOL_VERSIONS}. "
|
f"PROTOCOL_VERSION={PROTOCOL_VERSION} is not in KNOWN_PROTOCOL_VERSIONS={KNOWN_PROTOCOL_VERSIONS}. " +
|
||||||
+ "If this bump is intentional, add it to KNOWN_PROTOCOL_VERSIONS."
|
"If this bump is intentional, add it to KNOWN_PROTOCOL_VERSIONS."
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_protocol_version_matches_latest_known(self):
|
def test_protocol_version_matches_latest_known(self):
|
||||||
assert PROTOCOL_VERSION == LATEST_KNOWN, "Test invariant: PROTOCOL_VERSION must equal max(KNOWN_PROTOCOL_VERSIONS)."
|
assert PROTOCOL_VERSION == LATEST_KNOWN, (
|
||||||
|
"Test invariant: PROTOCOL_VERSION must equal max(KNOWN_PROTOCOL_VERSIONS)."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestOpaquePerBrandFlags(OpenpilotTestCase):
|
class TestOpaquePerBrandFlags(OpenpilotTestCase):
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ isolates one of the gating bugs that the design-overhaul branch fixes so a
|
|||||||
future regression is loud and obvious. These tests are intentionally narrow
|
future regression is loud and obvious. These tests are intentionally narrow
|
||||||
and additive — they do not replace the broader test_settings_schema.py.
|
and additive — they do not replace the broader test_settings_schema.py.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -25,13 +24,14 @@ from openpilot.sunnypilot.sunnylink.tools.generate_settings_schema import (
|
|||||||
_load_torque_versions,
|
_load_torque_versions,
|
||||||
generate_schema,
|
generate_schema,
|
||||||
)
|
)
|
||||||
from openpilot.sunnypilot.sunnylink.tools.validate_settings_ui import validate as validate_settings_ui
|
|
||||||
from openpilot.common.test import OpenpilotTestCase
|
from openpilot.common.test import OpenpilotTestCase
|
||||||
|
|
||||||
|
|
||||||
|
SCHEMA_VALIDATOR_PATH = os.path.join(os.path.dirname(DEFINITION_PATH), "settings_ui.schema.json")
|
||||||
|
|
||||||
|
|
||||||
def _walk_items(schema: dict[str, Any]):
|
def _walk_items(schema: dict[str, Any]):
|
||||||
"""Yield every item dict from the schema."""
|
"""Yield every item dict from the schema."""
|
||||||
|
|
||||||
def _yield(item: dict[str, Any]):
|
def _yield(item: dict[str, Any]):
|
||||||
yield item
|
yield item
|
||||||
for sub in item.get("sub_items", []):
|
for sub in item.get("sub_items", []):
|
||||||
@@ -149,13 +149,22 @@ class TestTestManeuversSection(OpenpilotTestCase):
|
|||||||
assert "is_sp_release" in vis_refs
|
assert "is_sp_release" in vis_refs
|
||||||
enablement = section.get("enablement") or []
|
enablement = section.get("enablement") or []
|
||||||
enable_refs = json.dumps(enablement)
|
enable_refs = json.dumps(enablement)
|
||||||
assert "ShowAdvancedControls" in enable_refs, "test_maneuvers must gate ShowAdvancedControls via enablement"
|
assert "ShowAdvancedControls" in enable_refs, \
|
||||||
|
"test_maneuvers must gate ShowAdvancedControls via enablement"
|
||||||
|
|
||||||
|
|
||||||
class TestValidator(OpenpilotTestCase):
|
class TestValidator(OpenpilotTestCase):
|
||||||
def test_validator_accepts_real_json(self):
|
def test_validator_accepts_real_json(self):
|
||||||
"""settings_ui.json passes the repository's production schema validator."""
|
"""settings_ui.json validates against settings_ui.schema.json."""
|
||||||
self.assertTrue(validate_settings_ui(DEFINITION_PATH))
|
try:
|
||||||
|
import jsonschema
|
||||||
|
except ImportError:
|
||||||
|
self.skipTest("jsonschema not installed")
|
||||||
|
with open(DEFINITION_PATH) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
with open(SCHEMA_VALIDATOR_PATH) as f:
|
||||||
|
validator = json.load(f)
|
||||||
|
jsonschema.validate(instance=data, schema=validator)
|
||||||
|
|
||||||
|
|
||||||
class TestTorqueOptionGeneration(OpenpilotTestCase):
|
class TestTorqueOptionGeneration(OpenpilotTestCase):
|
||||||
@@ -168,17 +177,16 @@ class TestTorqueOptionGeneration(OpenpilotTestCase):
|
|||||||
assert item.get("options") == expected
|
assert item.get("options") == expected
|
||||||
|
|
||||||
def test_torque_versions_path_resolves(self):
|
def test_torque_versions_path_resolves(self):
|
||||||
assert os.path.exists(TORQUE_VERSIONS_PATH), f"latcontrol_torque_versions.json not found at {TORQUE_VERSIONS_PATH}"
|
assert os.path.exists(TORQUE_VERSIONS_PATH), (
|
||||||
|
f"latcontrol_torque_versions.json not found at {TORQUE_VERSIONS_PATH}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestReleaseBranchGates(OpenpilotTestCase):
|
class TestReleaseBranchGates(OpenpilotTestCase):
|
||||||
@parameterized.expand(
|
@parameterized.expand([
|
||||||
[
|
"EnableGithubRunner",
|
||||||
"EnableGithubRunner",
|
"QuickBootToggle",
|
||||||
"QuickBootToggle",
|
], names=["key"])
|
||||||
],
|
|
||||||
names=["key"],
|
|
||||||
)
|
|
||||||
def test_sp_dev_items_gate_on_is_sp_release(self, schema, key):
|
def test_sp_dev_items_gate_on_is_sp_release(self, schema, key):
|
||||||
"""sunnypilot dev items must hide on sunnypilot release branches (is_sp_release gate)."""
|
"""sunnypilot dev items must hide on sunnypilot release branches (is_sp_release gate)."""
|
||||||
item = _find_item(schema, key)
|
item = _find_item(schema, key)
|
||||||
@@ -200,14 +208,11 @@ class TestSpuriousOffroadGatesDropped(OpenpilotTestCase):
|
|||||||
|
|
||||||
|
|
||||||
class TestNotEngagedReplacement(OpenpilotTestCase):
|
class TestNotEngagedReplacement(OpenpilotTestCase):
|
||||||
@parameterized.expand(
|
@parameterized.expand([
|
||||||
[
|
"AlphaLongitudinalEnabled",
|
||||||
"AlphaLongitudinalEnabled",
|
"ToyotaEnforceStockLongitudinal",
|
||||||
"ToyotaEnforceStockLongitudinal",
|
"ToyotaStopAndGoHack",
|
||||||
"ToyotaStopAndGoHack",
|
], names=["key"])
|
||||||
],
|
|
||||||
names=["key"],
|
|
||||||
)
|
|
||||||
def test_offroad_only_replaced_with_not_engaged(self, schema, key):
|
def test_offroad_only_replaced_with_not_engaged(self, schema, key):
|
||||||
"""These items should use not_engaged, not offroad_only."""
|
"""These items should use not_engaged, not offroad_only."""
|
||||||
item = _find_item(schema, key)
|
item = _find_item(schema, key)
|
||||||
@@ -215,5 +220,3 @@ class TestNotEngagedReplacement(OpenpilotTestCase):
|
|||||||
rule_types = _flatten_rule_types(item.get("enablement"))
|
rule_types = _flatten_rule_types(item.get("enablement"))
|
||||||
assert "offroad_only" not in rule_types, f"{key} still uses offroad_only"
|
assert "offroad_only" not in rule_types, f"{key} still uses offroad_only"
|
||||||
assert "not_engaged" in rule_types, f"{key} missing not_engaged"
|
assert "not_engaged" in rule_types, f"{key} missing not_engaged"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -276,36 +276,13 @@ class TestKnownPanels(OpenpilotTestCase):
|
|||||||
enhanced_enable_keys = {r.get("key") for r in enhanced.get("enablement", []) if r.get("type") == "param"}
|
enhanced_enable_keys = {r.get("key") for r in enhanced.get("enablement", []) if r.get("type") == "param"}
|
||||||
assert "NeuralNetworkLateralControl" in enhanced_enable_keys
|
assert "NeuralNetworkLateralControl" in enhanced_enable_keys
|
||||||
|
|
||||||
def test_accel_controller_profile_mapping_and_enablement(self, schema):
|
|
||||||
cruise = next(p for p in schema["panels"] if p["id"] == "cruise")
|
|
||||||
items = {item["key"]: item for item in _iter_panel_items(cruise)}
|
|
||||||
|
|
||||||
assert items["AccelPersonalityEnabled"]["widget"] == "toggle"
|
|
||||||
assert items["AccelPersonality"]["options"] == [
|
|
||||||
{"value": 0, "label": "Eco"},
|
|
||||||
{"value": 1, "label": "Normal"},
|
|
||||||
{"value": 2, "label": "Sport"},
|
|
||||||
]
|
|
||||||
assert {
|
|
||||||
"type": "capability",
|
|
||||||
"field": "has_longitudinal_control",
|
|
||||||
"equals": True,
|
|
||||||
} in items["AccelPersonalityEnabled"]["enablement"]
|
|
||||||
assert {
|
|
||||||
"type": "capability",
|
|
||||||
"field": "has_longitudinal_control",
|
|
||||||
"equals": True,
|
|
||||||
} in items["AccelPersonality"]["enablement"]
|
|
||||||
profile_enable_keys = {rule.get("key") for rule in items["AccelPersonality"]["enablement"] if rule.get("type") == "param"}
|
|
||||||
assert "AccelPersonalityEnabled" not in profile_enable_keys
|
|
||||||
|
|
||||||
|
|
||||||
class TestKnownVehicleSettings(OpenpilotTestCase):
|
class TestKnownVehicleSettings(OpenpilotTestCase):
|
||||||
def test_hyundai_has_longitudinal_tuning(self, schema):
|
def test_hyundai_has_longitudinal_tuning(self, schema):
|
||||||
keys = {i["key"] for i in _brand_items(schema["vehicle_settings"].get("hyundai"))}
|
keys = {i["key"] for i in _brand_items(schema["vehicle_settings"].get("hyundai"))}
|
||||||
assert "HyundaiLongitudinalTuning" in keys
|
assert "HyundaiLongitudinalTuning" in keys
|
||||||
|
|
||||||
def test_toyota_has_enforce_stock_stop_go(self, schema):
|
def test_toyota_has_enforce_stock_and_stop_go(self, schema):
|
||||||
keys = {i["key"] for i in _brand_items(schema["vehicle_settings"].get("toyota"))}
|
keys = {i["key"] for i in _brand_items(schema["vehicle_settings"].get("toyota"))}
|
||||||
assert "ToyotaEnforceStockLongitudinal" in keys
|
assert "ToyotaEnforceStockLongitudinal" in keys
|
||||||
assert "ToyotaStopAndGoHack" in keys
|
assert "ToyotaStopAndGoHack" in keys
|
||||||
|
|||||||
@@ -45,9 +45,8 @@ class ScrollState(Enum):
|
|||||||
|
|
||||||
|
|
||||||
class GuiScrollPanel2:
|
class GuiScrollPanel2:
|
||||||
def __init__(self, horizontal: bool = True, handle_out_of_bounds: bool = True) -> None:
|
def __init__(self, horizontal: bool = True) -> None:
|
||||||
self._horizontal = horizontal
|
self._horizontal = horizontal
|
||||||
self._handle_out_of_bounds = handle_out_of_bounds
|
|
||||||
self._state = ScrollState.STEADY
|
self._state = ScrollState.STEADY
|
||||||
self._offset: rl.Vector2 = rl.Vector2(0, 0)
|
self._offset: rl.Vector2 = rl.Vector2(0, 0)
|
||||||
self._initial_click_event: MouseEvent | None = None
|
self._initial_click_event: MouseEvent | None = None
|
||||||
@@ -86,20 +85,6 @@ class GuiScrollPanel2:
|
|||||||
"""Returns (max_offset, min_offset) for the given bounds and content size."""
|
"""Returns (max_offset, min_offset) for the given bounds and content size."""
|
||||||
return 0.0, min(0.0, bounds_size - content_size)
|
return 0.0, min(0.0, bounds_size - content_size)
|
||||||
|
|
||||||
def _clamp_offset(self, bounds_size: float, content_size: float) -> None:
|
|
||||||
if self._handle_out_of_bounds:
|
|
||||||
return
|
|
||||||
|
|
||||||
max_offset, min_offset = self._get_offset_bounds(bounds_size, content_size)
|
|
||||||
offset = self.get_offset()
|
|
||||||
clamped_offset = max(min_offset, min(max_offset, offset))
|
|
||||||
if clamped_offset == offset:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.set_offset(clamped_offset)
|
|
||||||
if (clamped_offset == max_offset and self._velocity > 0) or (clamped_offset == min_offset and self._velocity < 0):
|
|
||||||
self._velocity = 0.0
|
|
||||||
|
|
||||||
def _update_state(self, bounds_size: float, content_size: float, snap_target: float | None) -> None:
|
def _update_state(self, bounds_size: float, content_size: float, snap_target: float | None) -> None:
|
||||||
"""Runs per render frame, independent of mouse events. Updates auto-scrolling state and velocity."""
|
"""Runs per render frame, independent of mouse events. Updates auto-scrolling state and velocity."""
|
||||||
max_offset, min_offset = self._get_offset_bounds(bounds_size, content_size)
|
max_offset, min_offset = self._get_offset_bounds(bounds_size, content_size)
|
||||||
@@ -153,8 +138,6 @@ class GuiScrollPanel2:
|
|||||||
factor = 1.0 - math.exp(-SNAP_RATE * dt)
|
factor = 1.0 - math.exp(-SNAP_RATE * dt)
|
||||||
self.set_offset(self.get_offset() + dist * factor)
|
self.set_offset(self.get_offset() + dist * factor)
|
||||||
|
|
||||||
self._clamp_offset(bounds_size, content_size)
|
|
||||||
|
|
||||||
def _handle_mouse_event(self, mouse_event: MouseEvent, bounds: rl.Rectangle, bounds_size: float,
|
def _handle_mouse_event(self, mouse_event: MouseEvent, bounds: rl.Rectangle, bounds_size: float,
|
||||||
content_size: float) -> None:
|
content_size: float) -> None:
|
||||||
max_offset, min_offset = self._get_offset_bounds(bounds_size, content_size)
|
max_offset, min_offset = self._get_offset_bounds(bounds_size, content_size)
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ class _Scroller(Widget):
|
|||||||
self._items: list[Widget] = []
|
self._items: list[Widget] = []
|
||||||
self._horizontal = horizontal
|
self._horizontal = horizontal
|
||||||
self._snap_items = snap_items
|
self._snap_items = snap_items
|
||||||
|
assert not self._snap_items or self._horizontal, "Snapping is only supported for horizontal scrolling"
|
||||||
self._spacing = spacing
|
self._spacing = spacing
|
||||||
self._pad = pad
|
self._pad = pad
|
||||||
|
|
||||||
@@ -190,20 +191,12 @@ class _Scroller(Widget):
|
|||||||
snap_target: float | None = None
|
snap_target: float | None = None
|
||||||
if self._snap_items and visible_items and self._scrolling_to[0] is None:
|
if self._snap_items and visible_items and self._scrolling_to[0] is None:
|
||||||
# TODO: this doesn't handle two small buttons at the edges well
|
# TODO: this doesn't handle two small buttons at the edges well
|
||||||
center_pos = (self._rect.x + self._rect.width / 2) if self._horizontal else (self._rect.y + self._rect.height / 2)
|
center_pos = self._rect.x + self._rect.width / 2
|
||||||
closest_delta_pos = min(
|
closest_delta_pos = min((((item.rect.x + item.rect.width / 2) - center_pos) for item in visible_items), key=abs)
|
||||||
(self._item_center_pos(item) - center_pos for item in visible_items),
|
|
||||||
key=abs,
|
|
||||||
)
|
|
||||||
snap_target = self.scroll_panel.get_offset() - closest_delta_pos
|
snap_target = self.scroll_panel.get_offset() - closest_delta_pos
|
||||||
|
|
||||||
return self.scroll_panel.update(self._rect, content_size, snap_target=snap_target)
|
return self.scroll_panel.update(self._rect, content_size, snap_target=snap_target)
|
||||||
|
|
||||||
def _item_center_pos(self, item: Widget) -> float:
|
|
||||||
if self._horizontal:
|
|
||||||
return item.rect.x + item.rect.width / 2
|
|
||||||
return item.rect.y + item.rect.height / 2
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def moving_items(self) -> bool:
|
def moving_items(self) -> bool:
|
||||||
return len(self._move_animations) > 0 or len(self._move_lift) > 0
|
return len(self._move_animations) > 0 or len(self._move_lift) > 0
|
||||||
|
|||||||
@@ -381,20 +381,20 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deepmerge"
|
name = "deepmerge"
|
||||||
version = "2.1.0"
|
version = "3.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/78/6e9e20106224083cfb817d2d3c26e80e72258d617b616721a169b87081e0/deepmerge-2.1.0.tar.gz", hash = "sha256:07ca7a7b8935df596c512fa8161877c0487ac61f691c07766e7d71d2b23bdd2f", size = 21449, upload-time = "2026-06-22T05:46:07.669Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/b7/6c/9f4577a36d5f463a3a3f8322bd65d33e1a1a6b6ba1d692a5ebc3cba19015/deepmerge-3.0.tar.gz", hash = "sha256:14ed69f063de64b7743985c732ccff5d6c34ff4560946e7fbfd99086b853b9ce", size = 22279, upload-time = "2026-08-17T05:50:53.161Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/51/25/2a75b47cb057b1e164c604fb81ab690a6cdb5e2260ce651194eae90f64a3/deepmerge-2.1.0-py3-none-any.whl", hash = "sha256:8f148339a91d680a75ecb74ade235d9e759a93df373a0b04e9d31c8666cfeb75", size = 14345, upload-time = "2026-06-22T05:46:06.742Z" },
|
{ url = "https://files.pythonhosted.org/packages/a8/d7/7f19bedd30b90b72865aeec3a29127bed6dee6c9ef0324bb5b4d424bb0e3/deepmerge-3.0-py3-none-any.whl", hash = "sha256:c8541c3e186dc88d19a5513ad3a0b2d0b22beaa780969fc0c13b995a64265365", size = 14855, upload-time = "2026-08-17T05:50:52.218Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "filelock"
|
name = "filelock"
|
||||||
version = "3.32.2"
|
version = "3.32.3"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/57/3ba6e6cb097f85b855b00163d169f35365f44277df044dcf96d55b8f62a3/filelock-3.32.2.tar.gz", hash = "sha256:c33351e1f49cae33414acbc6d56784e6ecee82514ec90795da1161fc4836b5b8", size = 217172, upload-time = "2026-07-29T22:46:04.895Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/7d/64/a02e6765de08964ed371eca577870593245afc9dfac16d037de7c10d18e6/filelock-3.32.3.tar.gz", hash = "sha256:0ffa185a3540854c95caa7fa76b76cb219d907415e2c5dc9af25fd970563487f", size = 218135, upload-time = "2026-08-13T16:00:05.577Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/c1/e8/72f8cef9fdfeffe06213fe8508039396ee48daa0e3259457ed766173bfd6/filelock-3.32.2-py3-none-any.whl", hash = "sha256:87dd94cf281e586d135fa51132b8e3d9a598b316e90377a288663c9321036c82", size = 98830, upload-time = "2026-07-29T22:46:03.52Z" },
|
{ url = "https://files.pythonhosted.org/packages/a7/8e/50f46a9c0ce8d2861a394c1347caae037ea0431d2f67d7feb151cbc4649a/filelock-3.32.3-py3-none-any.whl", hash = "sha256:7f0ca4bcc0e181c60dbbd8aa9ab5b120ebb99e4e064e83636340056f833a1f09", size = 98901, upload-time = "2026-08-13T16:00:03.974Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -478,7 +478,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "huggingface-hub"
|
name = "huggingface-hub"
|
||||||
version = "1.27.0"
|
version = "1.28.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
@@ -491,18 +491,18 @@ dependencies = [
|
|||||||
{ name = "tqdm" },
|
{ name = "tqdm" },
|
||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/3e/9b/ddf3d02a8681f1b9ce52fda03d755dad6b74c4f8172304c4c8d2975450f9/huggingface_hub-1.27.0.tar.gz", hash = "sha256:c1fed40ea82a6b41b477f5243546549b792ae0a93abcea608cff66089bf8f8df", size = 942668, upload-time = "2026-08-07T12:48:05.161Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/c6/ae/222a91937ebee7f62c0ca8f5ee0afd97577caf24c0abb927d1f5c7e9f6d2/huggingface_hub-1.28.0.tar.gz", hash = "sha256:46a2e950c09234de54093d587d1675382f0d08dbd600d9fb599b5932f5b2c6cb", size = 959609, upload-time = "2026-08-18T12:27:15.101Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/de/d8/95b735e183957c1f26d94c52977f09d466d55119cbbc1558ea4975e4c216/huggingface_hub-1.27.0-py3-none-any.whl", hash = "sha256:7df6827c2f956c60fbaa64646e979e566db76f619dd0a9729dfb8c5a3eb4f68d", size = 784926, upload-time = "2026-08-07T12:48:02.905Z" },
|
{ url = "https://files.pythonhosted.org/packages/51/0e/eafef18f1a75e125e68395db21131db0cf868a128ecd2fce69b4df6c584b/huggingface_hub-1.28.0-py3-none-any.whl", hash = "sha256:58a8bacb03072edfc38067065e9dc24bbb34805410fcd36a1632de0b329660bb", size = 793202, upload-time = "2026-08-18T12:27:12.719Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "3.18"
|
version = "3.19"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
|
{ url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -971,11 +971,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pygments"
|
name = "pygments"
|
||||||
version = "2.20.0"
|
version = "2.21.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
{ url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1194,18 +1194,18 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sounddevice"
|
name = "sounddevice"
|
||||||
version = "0.5.5"
|
version = "0.5.6"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "cffi" },
|
{ name = "cffi" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/f9/2592608737553638fca98e21e54bfec40bf577bb98a61b2770c912aab25e/sounddevice-0.5.5.tar.gz", hash = "sha256:22487b65198cb5bf2208755105b524f78ad173e5ab6b445bdab1c989f6698df3", size = 143191, upload-time = "2026-01-23T18:36:43.529Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/ec/db/0c890e2d9aab9ba284021efc02e1d3aebfecab1b611762d7434602209bcf/sounddevice-0.5.6.tar.gz", hash = "sha256:8ec9fbfde2e32f020b167e348f3ab3bac6625a5f15af524d790108ac7147a410", size = 1120094, upload-time = "2026-08-17T07:55:05.048Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/1e/0a/478e441fd049002cf308520c0d62dd8333e7c6cc8d997f0dda07b9fbcc46/sounddevice-0.5.5-py3-none-any.whl", hash = "sha256:30ff99f6c107f49d25ad16a45cacd8d91c25a1bcdd3e81a206b921a3a6405b1f", size = 32807, upload-time = "2026-01-23T18:36:35.649Z" },
|
{ url = "https://files.pythonhosted.org/packages/72/1f/62eef605172bddc1017508469a12f75bc7c4194ece35c734f822795f53b1/sounddevice-0.5.6-py3-none-any.whl", hash = "sha256:de099612311ad81e55d31ccbd83f43ea6bf4d87b48f9b6ea55a1fbcde0eee4e0", size = 32793, upload-time = "2026-08-17T07:54:57.507Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/56/f9/c037c35f6d0b6bc3bc7bfb314f1d6f1f9a341328ef47cd63fc4f850a7b27/sounddevice-0.5.5-py3-none-macosx_10_6_x86_64.macosx_10_6_universal2.whl", hash = "sha256:05eb9fd6c54c38d67741441c19164c0dae8ce80453af2d8c4ad2e7823d15b722", size = 108557, upload-time = "2026-01-23T18:36:37.41Z" },
|
{ url = "https://files.pythonhosted.org/packages/b6/84/85e719d49cf98b2f406d9ac9c338892286c4448eb42ef0b2625ccf159616/sounddevice-0.5.6-py3-none-macosx_10_6_x86_64.macosx_10_6_universal2.whl", hash = "sha256:e3aef00ad8b1d1740eb66d9a7671eab88a4d2b8fa4ab33498d742e63b65c309c", size = 1009647, upload-time = "2026-08-17T07:54:58.814Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/88/a1/d19dd9889cd4bce2e233c4fac007cd8daaf5b9fe6e6a5d432cf17be0b807/sounddevice-0.5.5-py3-none-win32.whl", hash = "sha256:1234cc9b4c9df97b6cbe748146ae0ec64dd7d6e44739e8e42eaa5b595313a103", size = 317765, upload-time = "2026-01-23T18:36:39.047Z" },
|
{ url = "https://files.pythonhosted.org/packages/c5/6f/6292145099f72a153a710245f46ae43e5fb6c77bec1b6086cb76c12dc280/sounddevice-0.5.6-py3-none-win32.whl", hash = "sha256:b36b807eb02abd257198bf84b2af05e4fea199a9d2f0019014169c7136d45e9c", size = 1009627, upload-time = "2026-08-17T07:55:00.401Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c3/0e/002ed7c4c1c2ab69031f78989d3b789fee3a7fba9e586eb2b81688bf4961/sounddevice-0.5.5-py3-none-win_amd64.whl", hash = "sha256:cfc6b2c49fb7f555591c78cb8ecf48d6a637fd5b6e1db5fec6ed9365d64b3519", size = 365324, upload-time = "2026-01-23T18:36:40.496Z" },
|
{ url = "https://files.pythonhosted.org/packages/8d/3e/cbc593c31a5f0d817b3fe97e64aa8461bd0f55cb07b67ce1b776296ae336/sounddevice-0.5.6-py3-none-win_amd64.whl", hash = "sha256:7f4162f514f007b0bf25a3ccfed3f1705bc2ec311888a90232729eec4f57a4f4", size = 1009630, upload-time = "2026-08-17T07:55:02.088Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/4e/39/a61d4b83a7746b70d23d9173be688c0c6bfc7173772344b7442c2c155497/sounddevice-0.5.5-py3-none-win_arm64.whl", hash = "sha256:3861901ddd8230d2e0e8ae62ac320cdd4c688d81df89da036dcb812f757bb3e6", size = 317115, upload-time = "2026-01-23T18:36:42.235Z" },
|
{ url = "https://files.pythonhosted.org/packages/60/a4/b0c21c9f215a6fd9606b8f8748c21212dc098e5d5a2d93068c50edcf19b4/sounddevice-0.5.6-py3-none-win_arm64.whl", hash = "sha256:c8ae19173e5f27f8c12d4b5eee2dbfe542cee125d591e663e0fb4dfb75246d45", size = 1009630, upload-time = "2026-08-17T07:55:03.689Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1335,27 +1335,27 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ty"
|
name = "ty"
|
||||||
version = "0.0.72"
|
version = "0.0.73"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/d5/df/656e684bafb13c1d146e7d5b5f3e7978ca177232acc84998ff36427e9462/ty-0.0.72.tar.gz", hash = "sha256:ec2b8066b618df18cab4cb8e992f8da45d360332acb23fa34df7fa29cd1b9d3a", size = 6654939, upload-time = "2026-08-14T21:35:42.612Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/e5/90/c4e1bb4cead3b644c3e258a27f9b05c7dc5eb0ec96a4f5282194edae9e0d/ty-0.0.73.tar.gz", hash = "sha256:823d4ce0d237bfc7eb6bcee70842f2c0706113813a16951077840743712f4b74", size = 6712739, upload-time = "2026-08-19T03:12:43.381Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/e2/3b/f51461239a4e66565d4b362f97a3b55fe7fdba2e944068341f87c62f6743/ty-0.0.72-py3-none-linux_armv6l.whl", hash = "sha256:fda86db153ffd85ee52000cf175d6a3f1c0223772cf7c5b6f726200bf92c7b44", size = 12621989, upload-time = "2026-08-14T21:35:01.676Z" },
|
{ url = "https://files.pythonhosted.org/packages/e4/0f/f5e1801e55cc631f2db193276675b30561b963a2403da832bffb5d100267/ty-0.0.73-py3-none-linux_armv6l.whl", hash = "sha256:90a946082bf9bc446b5e72973d9f4ff1222a240b2ca4c9e6eed61eb913e30810", size = 12715452, upload-time = "2026-08-19T03:12:06.673Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ca/fb/79ddf683affc679ca856f3510b5640ec3a88a842ba5f654f5d4bc78f1786/ty-0.0.72-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ceb944c612529b9023acfdc9cf4c0dcbb722549f9d17d46baecd1141baf01d7f", size = 12233910, upload-time = "2026-08-14T21:35:04.334Z" },
|
{ url = "https://files.pythonhosted.org/packages/54/32/515dd05074c213b433524ab97eb003b0132ae7e358e0d75633ba7a314ed8/ty-0.0.73-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b7d6b5c6a6db7ea95fbbc16af514ef44a27a29a2fe1dc798900790364d170209", size = 12301870, upload-time = "2026-08-19T03:12:08.924Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5d/45/10562a0d84802158db8fa4ec46de54aa9fdcecdeeaabbfe3639ae7042b66/ty-0.0.72-py3-none-macosx_11_0_arm64.whl", hash = "sha256:108d76218333d6c092e5f1cebf8e9b06f25738613a0236a28e2dd47c936ee52c", size = 12084108, upload-time = "2026-08-14T21:35:06.686Z" },
|
{ url = "https://files.pythonhosted.org/packages/50/4d/085b4889f0d4bbe4af8b96242d4a1cb209fff95967cfa239ea141983719b/ty-0.0.73-py3-none-macosx_11_0_arm64.whl", hash = "sha256:dd6f657f463e01372d8688f235be164750c8db722c97da27fa4903aa8d40b203", size = 12111741, upload-time = "2026-08-19T03:12:11.067Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a1/dc/1fe1aef8d697e3509face271a5331700c7aa1d1e44a4b622707bdfa41d4b/ty-0.0.72-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f3943f186f741a2499a31053872169250c9264a9a49684920e48d8fcf4ef4f5", size = 12132640, upload-time = "2026-08-14T21:35:09.305Z" },
|
{ url = "https://files.pythonhosted.org/packages/95/f6/d6ec277cadfecf03ad4c18551b67c4c6eb7807a0560d801db14be99d7a89/ty-0.0.73-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc2de468e33fd44c9ff1c43473a7316f4289480f5cba8995a67b6d22aee39ca9", size = 12196124, upload-time = "2026-08-19T03:12:13.14Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/14/46/41ceb265e96969487311a2014bd0e53abb4fbc1395efb2ebe411fcb4db62/ty-0.0.72-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf283c07dc3cc52ca48a3ad8ab100fb5aec3aebbd03ef6a12d5f910b8e596fc5", size = 12402489, upload-time = "2026-08-14T21:35:11.555Z" },
|
{ url = "https://files.pythonhosted.org/packages/75/b7/ce78d8707563af9cae9bbd25328bfbc4931035085bd20089adf0c418f70e/ty-0.0.73-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2942fa0ef795a66034cdc8d75a72f453442f3b58ff2f69b4da05b7b954765b55", size = 12488557, upload-time = "2026-08-19T03:12:15.252Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/45/30bf43cb4fd505c5c2dd30fda27dde5f05208686cd21217adec77c954204/ty-0.0.72-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:95f3b6462c38f9f115d10cee21f47fedf715fcf2040daf36eef210359300bc7c", size = 13130835, upload-time = "2026-08-14T21:35:13.746Z" },
|
{ url = "https://files.pythonhosted.org/packages/d8/e8/329b9851b23502758c5c98e8cc875ea2a1b4c9674b4ca3a86da56a5063d3/ty-0.0.73-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e0f1ef14f642e18ac4e7a616a2796dcf7a5d82e28cd17f9796494acc7c4aabb", size = 13215606, upload-time = "2026-08-19T03:12:17.225Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/31/2f/03bba754d2613f640df168335c41f83f41db150bb515839c60d80e3a7880/ty-0.0.72-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30caf658feb8ffb250d9e9e47107657a78f5f3425c227df1664d8df2ebe38880", size = 13590392, upload-time = "2026-08-14T21:35:16.839Z" },
|
{ url = "https://files.pythonhosted.org/packages/36/38/67fedfd2cb77516ef0066b1642f487dba0eb3006493cf3475b15f5b8b228/ty-0.0.73-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16981e15fdceedb37d0aff76c5ac25914595dfee2675af95335550064251ad22", size = 13665497, upload-time = "2026-08-19T03:12:19.286Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/04/c7/03c67f00e63005ec41585653dc3096064570b1e6273742baae2798cd242f/ty-0.0.72-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27bdc012ddfbeec8948e4a6036c0dc39ac7cf2c8ec7c7d48dc7d2fd56d57b399", size = 13309629, upload-time = "2026-08-14T21:35:19.169Z" },
|
{ url = "https://files.pythonhosted.org/packages/8e/b3/154f4dd48ec5eebc186ab4b822c6e62f982fc5ddfd262d6e3903c2acba44/ty-0.0.73-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:644b2bec8a2e2e4957a942ae81d6cff5571c489bb5a8675e4d3886de537a694d", size = 13351231, upload-time = "2026-08-19T03:12:21.353Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c1/df/102d3b264eb7f2a58dd11952f229bb5150bb5668d176a6154976a6675981/ty-0.0.72-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:802c5970a77d7739e6f499921fbb6984fb7ad8a31d95e1ff42fd46f3642e4f3b", size = 12734028, upload-time = "2026-08-14T21:35:22.099Z" },
|
{ url = "https://files.pythonhosted.org/packages/35/5f/d462496903fbe453fb76363f8478be929c8e6ff21e6928c57dcd7e5fa21f/ty-0.0.73-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:338d565be3186f50ff8e9d10483685549c2d23f0754485d5ede3b54f4319188a", size = 12782586, upload-time = "2026-08-19T03:12:23.667Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/61/85/d0737c8c54d0ba67366ddfb9f31d88edf0b02299e65923e6945ae60ebcb5/ty-0.0.72-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:47dce65114fdc615c68ca0edb393b433df0956447e4267df0e264137a789598d", size = 13174832, upload-time = "2026-08-14T21:35:24.71Z" },
|
{ url = "https://files.pythonhosted.org/packages/87/52/ec6d24b74abe3ec324204c1c71e6d0c6c76a17ffc15fd51d603b0a302abe/ty-0.0.73-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:11c7b6d839309d2c102cb3a4c03d817176bbfab5b2fccc95a75ec5c9597421c9", size = 13247134, upload-time = "2026-08-19T03:12:25.956Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/1e/31/497f5a96c36d9b586ab6afe0574986835c6fd5b835a89773d2bec4711b49/ty-0.0.72-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:325144fa07e2675d0faa337fcc864213c272a499eb0cfe5bde2fdc62282d27bc", size = 12215005, upload-time = "2026-08-14T21:35:26.892Z" },
|
{ url = "https://files.pythonhosted.org/packages/26/20/cc74650fec56a54786c6d7c89e09576fcad3092be34cf21715d39a406a9b/ty-0.0.73-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:488572db7ff97fb50ea36a76250f2d617c9727d143da6c7bf0623276eb0fc507", size = 12309344, upload-time = "2026-08-19T03:12:28.122Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/df/7d/46e65b17b4966c7cd0140f134380d33d8e84fe6efccd761533ce793dc502/ty-0.0.72-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a5c9f15d0f58e43707d8848274be1821a0ef408eccb8aa7dda28a4a9eddf7640", size = 12421298, upload-time = "2026-08-14T21:35:29.301Z" },
|
{ url = "https://files.pythonhosted.org/packages/89/bd/4b0a9087f4315d7fbadf77a3ce44c816cc9ffabed1ced06cc5be81fbc414/ty-0.0.73-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1b958ebceefbbf594e59eb8d3d55bbd033ce634026fcba3e4bc3179e78e45bb7", size = 12502319, upload-time = "2026-08-19T03:12:30.128Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/08/2a/12ada4ec17700b3cb1d4fd3bc3e5b1852df9e6885288429318cade87b3c1/ty-0.0.72-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8ee508d64b381871529cc22c412b41071bf5e908b7aa5d66a38f3f6b2573a806", size = 12669242, upload-time = "2026-08-14T21:35:31.444Z" },
|
{ url = "https://files.pythonhosted.org/packages/11/80/0a925074911fe111912ea29d9eed309bcc183f43d2fb3eef07db056a0beb/ty-0.0.73-py3-none-musllinux_1_2_i686.whl", hash = "sha256:91a32993b3c34e42c3f323ad6c0399cb596bd1c27e9b7f20db7cd64c1067b68e", size = 12753688, upload-time = "2026-08-19T03:12:32.433Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/1c/1a/4692536880790fb550ed6d44a6096778dc71bb112f2c6d615cebb01a57e5/ty-0.0.72-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3699e2ec7921d44da79d6b089f7bf239b2cc53c4e45a5a38430adc34ee9e9a55", size = 12988199, upload-time = "2026-08-14T21:35:33.749Z" },
|
{ url = "https://files.pythonhosted.org/packages/24/6b/aeccaf89efbc2e112bd415340a22e2669ec998aa397242503e747b712ca4/ty-0.0.73-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bab8a19fbf51f479bddb2a12c5fabfe52f918a5590362321ed5d89b44eb62c15", size = 13069050, upload-time = "2026-08-19T03:12:35.398Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9a/0d/f5e5a50322e9c45865e7b7a428ba6cd6527387cf0f2472492ac3cf746243/ty-0.0.72-py3-none-win32.whl", hash = "sha256:f25f72a67bd36cd247707c4784e52fad0b6b4f42a1b7dd14804110fa95c486ed", size = 11939708, upload-time = "2026-08-14T21:35:36.006Z" },
|
{ url = "https://files.pythonhosted.org/packages/d7/3e/eae485fd86c1585943fd4e1746b0757b2da01e2c43136ebe8c686fe1c7f1/ty-0.0.73-py3-none-win32.whl", hash = "sha256:03347a612f0fa020b19bfd8dbd521db6ecc75d377a3e4d4f6e6c2e62871da4cc", size = 12053187, upload-time = "2026-08-19T03:12:37.565Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3f/4e/8af3534b2e4214e6184a5a59c34101e94a68d578f081f97b995866bab1bf/ty-0.0.72-py3-none-win_amd64.whl", hash = "sha256:cdeee869341717e1736cea2e2d7856738c6957c320f584ed2f68c8f90100d2f5", size = 12643876, upload-time = "2026-08-14T21:35:38.141Z" },
|
{ url = "https://files.pythonhosted.org/packages/a7/01/9b8b983786e3ce34924e372e8b76b92b508273ab65c589fc7e88cc03ee17/ty-0.0.73-py3-none-win_amd64.whl", hash = "sha256:cedd05122ded0b5dcc55431a370e974b747f99c41c290a3d2ab8c1867f197519", size = 12693838, upload-time = "2026-08-19T03:12:39.483Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/ea/a2606e654c7276bd08586391a2525b0af3f3bf60228a8c57b2d248f273f9/ty-0.0.72-py3-none-win_arm64.whl", hash = "sha256:1bd3ac3ed4424a6d6990a85dc388556aea012bd752de21349a84b685951de0d8", size = 12394857, upload-time = "2026-08-14T21:35:40.277Z" },
|
{ url = "https://files.pythonhosted.org/packages/ea/88/25333bbfea6a5dc064371d2002d3d4807db90b84d5448f9106b2712b0fbc/ty-0.0.73-py3-none-win_arm64.whl", hash = "sha256:e47068f8369dea5d641a26a2ad0a947a320b02ff87099b07e95de0323245a4dc", size = 12443573, upload-time = "2026-08-19T03:12:41.449Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1387,7 +1387,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zensical"
|
name = "zensical"
|
||||||
version = "0.0.54"
|
version = "0.0.56"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
@@ -1399,20 +1399,20 @@ dependencies = [
|
|||||||
{ name = "pyyaml" },
|
{ name = "pyyaml" },
|
||||||
{ name = "tomli" },
|
{ name = "tomli" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/75/7e/343a78c0c9da1954d2f0a4d47ca778baac48bb18c6b9c0c6260e7974976e/zensical-0.0.54.tar.gz", hash = "sha256:4de205dbb323d0a443e2ebf3fef77e93e3c1493c34a58d205e7f3631dd7745af", size = 3992024, upload-time = "2026-08-13T16:04:49.297Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/f8/7d/18bb725a659352e9af0940a3d879c5edcff5d86fec3ac15ce500d484d9d3/zensical-0.0.56.tar.gz", hash = "sha256:c359163800d1c3a8c39af48f4e2869fcfc2b4fc00d28652bd2a5b0330c36530c", size = 3997416, upload-time = "2026-08-18T15:46:47.283Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/8b/cf/13e887c303fd5c786c09f83362382a42d10292fca633a95067de6a6591a3/zensical-0.0.54-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f7177a3b6647e4ee47864ab02e5141f9e923dd57b9fa96e5dc5da4228daff505", size = 12893082, upload-time = "2026-08-13T16:04:17.337Z" },
|
{ url = "https://files.pythonhosted.org/packages/8b/4b/6810aec6e670451f39639039b570a43d90bc1d4a3b93cf13316ccf6bad11/zensical-0.0.56-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:5135ea3aa5d1358503fc1903e866c191873b138028bc1ab170abac3a4b537ffe", size = 12874712, upload-time = "2026-08-18T15:46:18.378Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/82/ee/7fe1418fa31bc120cf9eb0fbce9e021c40752206ce993a7bc652c011f890/zensical-0.0.54-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:7b23c3b0c720885891b220c3e562074d93eb940314823d91875c6246976bd9b2", size = 12778626, upload-time = "2026-08-13T16:04:19.906Z" },
|
{ url = "https://files.pythonhosted.org/packages/97/98/23445d8ed708088dd6d9d51674f8836b77c53aab280360d7aa7a206bea8e/zensical-0.0.56-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:a22ae2329ba755c6e58e1fe5967ca429d580d346a73cf467ad5185377e2cf809", size = 12764552, upload-time = "2026-08-18T15:46:20.746Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/61/b8/0420115270c1a22a2d4a1598f89dadc4e933eb7aa85539b72f3532acc5b6/zensical-0.0.54-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c012eb0ec20fda5794e90b4906b7401c77b712e2acc7f9f65026935368539da", size = 13225462, upload-time = "2026-08-13T16:04:22.663Z" },
|
{ url = "https://files.pythonhosted.org/packages/14/bd/ab89450728b0a55e6a52a86060b38a362a52a1b9f02eda7fef68b729eb2e/zensical-0.0.56-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03c70ef328e31cce0e31739acdd6679e9272bc7a3016ca5eafaa16dcfda460c9", size = 13212920, upload-time = "2026-08-18T15:46:22.977Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8d/48/0dfdd3e00fb3b807de702383ef5f4e9cf0d2791fee0e8a56f39bd590f16b/zensical-0.0.54-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0e851d26ba4f7397db3b3532e534c0572a9826bbd451ee0a00e649c030dcb38", size = 13158184, upload-time = "2026-08-13T16:04:24.958Z" },
|
{ url = "https://files.pythonhosted.org/packages/bf/5a/969fd9a461204392a9266a544c185fbb223714b306534661dcbb7d290be7/zensical-0.0.56-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ffc50153a50292078357a5052e7a6b3e20a818523792ffcbeace70418b761eb", size = 13146652, upload-time = "2026-08-18T15:46:25.773Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b1/2c/f1fb1f5387108b206f985cdb394bbdc730557cc339e63067ded9b3565263/zensical-0.0.54-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a48583da6f485e2373845d4332882c9e9d504302dbd2731c589543584df5b087", size = 13536772, upload-time = "2026-08-13T16:04:27.373Z" },
|
{ url = "https://files.pythonhosted.org/packages/22/b3/28a7c8dea8fe2edbba75a664efbde797b5922651ea92ffc480947ab22197/zensical-0.0.56-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88bba0e339d36647ce638a42b4ba96f2521b59ea54be74c314340be7e401f94b", size = 13530946, upload-time = "2026-08-18T15:46:28.19Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a3/97/3224b3dd5d76cebddf9725ae871a5a6a8f2de177be59d802232d00073d8b/zensical-0.0.54-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da7781a906623fb7bc278cc874ec6f80691c8753b90fbdf90a451abb046ae204", size = 13191901, upload-time = "2026-08-13T16:04:30.295Z" },
|
{ url = "https://files.pythonhosted.org/packages/d6/a6/d55a18c6e041b788af1d19e4d8c61ee9b8a7de5b9cc79ffa7bc9565e3a28/zensical-0.0.56-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b7a37f6ac38ac218e3e0dd311b58c468603963e38ee23f2e25672dcd6e9c17b", size = 13178741, upload-time = "2026-08-18T15:46:30.378Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b2/00/b1f55530c8df4331e1f209ec605816a578142f124b5f8078a9d984514d63/zensical-0.0.54-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:75aff1c01f6104dd0e79d08c87bd595da13db3e1f6da55fc9933ebff3e94fdd5", size = 13402956, upload-time = "2026-08-13T16:04:32.899Z" },
|
{ url = "https://files.pythonhosted.org/packages/6c/4f/a07da2f761cfd6a27faf9e8d5a9475c396a9a0ca37424a0b67cab7ae4d2c/zensical-0.0.56-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5f6d850bce3184422b37b9d98ef4d123047a47446844793251cabc04bd55f8f0", size = 13389836, upload-time = "2026-08-18T15:46:32.847Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c7/6e/58df496c2742600df3a2f273ba52b8ced1ce357340b825af9f1bb0f2042e/zensical-0.0.54-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1469c5d551a0ea2d0fcf922046a263d2afcff5f03ce3bb443e286970d04ff9f2", size = 13431462, upload-time = "2026-08-13T16:04:35.518Z" },
|
{ url = "https://files.pythonhosted.org/packages/34/aa/697ef9846b0e2071de4d03b0472e2658380ea9271bd01235f4ffaeef1978/zensical-0.0.56-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:d18944a4a111050b8f571e73d4e2475d7ce62ce78b64f09bcf33bb11cc346e1c", size = 13419551, upload-time = "2026-08-18T15:46:35.112Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5b/85/70ae775db7865be2434bc39e9b4ff1c7988978d796a7ff9d49294e7410c7/zensical-0.0.54-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:31c354f98b3374b9bcab65278a278dbec66125c997f7f1a5ee9501f413b87b9c", size = 13587289, upload-time = "2026-08-13T16:04:37.954Z" },
|
{ url = "https://files.pythonhosted.org/packages/97/6b/20e1b2443951d5182b3fc2ee54c200ea00c09bd8901a479be4147c94361b/zensical-0.0.56-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:3937029ec5091d577c2a05ebccd38fde97fab28662d165ead66d566689b2a6df", size = 13579878, upload-time = "2026-08-18T15:46:37.381Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/0f/ca/05e6b3e04323810bbf4da9240b8b710740fac82ec01659b14c86e44aa88e/zensical-0.0.54-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:85ef75654e7845aa65f1392af771771566f33faac452a2aed004ba367454f812", size = 13534594, upload-time = "2026-08-13T16:04:41.006Z" },
|
{ url = "https://files.pythonhosted.org/packages/53/c7/9c3b400b8a7d78cc169b7a78d4f74a90f9114209034a604f04051f48037c/zensical-0.0.56-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:346a1cdbad157633d185f79039a97c8b4f8c2b40be7d7efd56d72622f40247b0", size = 13526142, upload-time = "2026-08-18T15:46:39.949Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/14/70/be34910c13632f85911f505bd9a4d1bb53d46c8498d20ebb18570bbe4b7b/zensical-0.0.54-cp310-abi3-win32.whl", hash = "sha256:b56c80a8cd234666afb917fb1ed8467104f6857b1acabd66f60ef1b8a0daa66f", size = 12448180, upload-time = "2026-08-13T16:04:43.486Z" },
|
{ url = "https://files.pythonhosted.org/packages/0e/f6/7a6f2a513054071e44a504d7157684f00ac5e5e5f741eadc78ab6c80642c/zensical-0.0.56-cp310-abi3-win32.whl", hash = "sha256:a06681046f74b5bdc506d22af8fcef505b2450e45538c7f01a5e028f4e50c6f7", size = 12433218, upload-time = "2026-08-18T15:46:42.329Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a1/c6/a965265946555023dc0e159a41038502190882669d177dfa59b1dd3d580b/zensical-0.0.54-cp310-abi3-win_amd64.whl", hash = "sha256:f5a602986c4123a349cfd075c8d494096a2a2db74422169d65f8092872e48dfa", size = 12712264, upload-time = "2026-08-13T16:04:46.155Z" },
|
{ url = "https://files.pythonhosted.org/packages/6b/ed/5b497c75fb1fd5845f3ec2f0b11b5e01f18f14c297041cd2f20d30d8b6a4/zensical-0.0.56-cp310-abi3-win_amd64.whl", hash = "sha256:c557985f12d042c15dcb7d577543d81ceee9281f96d591d265310b673437a325", size = 12702823, upload-time = "2026-08-18T15:46:44.836Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
Reference in New Issue
Block a user