mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-06-20 01:02:07 +08:00
Compare commits
28 Commits
master-dev
...
tn
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a167d5024 | |||
| 21e0583752 | |||
| 88dad50ee1 | |||
| 9f20c56b2f | |||
| a2b50e5a8b | |||
| bebe8f0b04 | |||
| aa23fd0e4a | |||
| 8f942eefd7 | |||
| 9487f880e2 | |||
| 17b097b434 | |||
| 17eca0ab43 | |||
| 634ba8f6b2 | |||
| a45487f829 | |||
| e790ce047b | |||
| 96d7850888 | |||
| 6bf721a8c9 | |||
| bc6dbf8ca1 | |||
| ffb7bbbbc4 | |||
| 83de89e253 | |||
| 04224e8747 | |||
| 6012ebb7c7 | |||
| e91dbe351e | |||
| e25061fd08 | |||
| baf56ae324 | |||
| 9badd3fa40 | |||
| 2220e7fc11 | |||
| e862935209 | |||
| 9bd504a5cb |
@@ -4,6 +4,7 @@
|
|||||||
[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/commaai/msgq.git
|
url = https://github.com/commaai/msgq.git
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ 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;
|
||||||
|
acceleration @8 :Acceleration;
|
||||||
|
|
||||||
struct DynamicExperimentalControl {
|
struct DynamicExperimentalControl {
|
||||||
state @0 :DynamicExperimentalControlState;
|
state @0 :DynamicExperimentalControlState;
|
||||||
@@ -296,6 +297,23 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
|
|||||||
greenLightAlert @0 :Bool;
|
greenLightAlert @0 :Bool;
|
||||||
leadDepartAlert @1 :Bool;
|
leadDepartAlert @1 :Bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Acceleration Personality (Eco / Normal / Sport)
|
||||||
|
struct Acceleration {
|
||||||
|
personality @0 :AccelerationPersonality;
|
||||||
|
enabled @1 :Bool;
|
||||||
|
maxAccel @2 :Float32; # current speed-indexed accel ceiling
|
||||||
|
brakeNeed @3 :Float32; # predicted decel demand from the lookahead (m/s^2, positive)
|
||||||
|
decelTarget @4 :Float32; # early-soft comfort decel target (m/s^2, negative)
|
||||||
|
smoothActive @5 :Bool; # early-soft braking currently shaping the target
|
||||||
|
bypassed @6 :Bool; # passthrough to stock plan (hard brake / FCW / should_stop / closing lead / e2e)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AccelerationPersonality {
|
||||||
|
eco @0;
|
||||||
|
normal @1;
|
||||||
|
sport @2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct OnroadEventSP @0xda96579883444c35 {
|
struct OnroadEventSP @0xda96579883444c35 {
|
||||||
@@ -342,6 +360,7 @@ struct OnroadEventSP @0xda96579883444c35 {
|
|||||||
speedLimitChanged @21;
|
speedLimitChanged @21;
|
||||||
speedLimitPending @22;
|
speedLimitPending @22;
|
||||||
e2eChime @23;
|
e2eChime @23;
|
||||||
|
laneChangeRoadEdge @24;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,6 +467,8 @@ struct LiveMapDataSP @0xf416ec09499d9d19 {
|
|||||||
|
|
||||||
struct ModelDataV2SP @0xa1680744031fdb2d {
|
struct ModelDataV2SP @0xa1680744031fdb2d {
|
||||||
laneTurnDirection @0 :TurnDirection;
|
laneTurnDirection @0 :TurnDirection;
|
||||||
|
leftLaneChangeEdgeBlock @1 :Bool;
|
||||||
|
rightLaneChangeEdgeBlock @2 :Bool;
|
||||||
|
|
||||||
enum TurnDirection {
|
enum TurnDirection {
|
||||||
none @0;
|
none @0;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
#include "cereal/gen/cpp/log.capnp.h"
|
#include "cereal/gen/cpp/log.capnp.h"
|
||||||
|
#include "cereal/gen/cpp/custom.capnp.h"
|
||||||
|
|
||||||
inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||||
{"AccessToken", {CLEAR_ON_MANAGER_START | DONT_LOG, STRING}},
|
{"AccessToken", {CLEAR_ON_MANAGER_START | DONT_LOG, STRING}},
|
||||||
@@ -179,12 +180,19 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"QuickBootToggle", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"QuickBootToggle", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"RainbowMode", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"RainbowMode", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
|
{"RoadEdgeLaneChangeEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"RocketFuel", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"RocketFuel", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"ShowAdvancedControls", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"ShowAdvancedControls", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"ShowTurnSignals", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"ShowTurnSignals", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"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"}},
|
||||||
@@ -228,6 +236,13 @@ 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"}},
|
||||||
|
|
||||||
|
// Acceleration Personality (Eco / Normal / Sport)
|
||||||
|
{"AccelPersonalityEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
|
{"AccelPersonality", {PERSISTENT | BACKUP, INT, std::to_string(static_cast<int>(cereal::LongitudinalPlanSP::AccelerationPersonality::NORMAL))}},
|
||||||
|
|
||||||
|
// Radar Distance: hold a lead through radar flicker/dropout so the MPC doesn't lose+regain it
|
||||||
|
{"RadarDistance", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
|
|
||||||
// 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"}},
|
||||||
|
|||||||
+1
-1
Submodule opendbc_repo updated: 10e654bf21...5ccb2753c1
@@ -10,7 +10,7 @@ from cereal import car, log, custom
|
|||||||
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.safety import ALTERNATIVE_EXPERIENCE
|
||||||
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
|
||||||
@@ -121,7 +121,13 @@ class Car:
|
|||||||
self.CI, self.CP, self.CP_SP = CI, CI.CP, CI.CP_SP
|
self.CI, self.CP, self.CP_SP = CI, CI.CP, CI.CP_SP
|
||||||
self.RI = RI
|
self.RI = RI
|
||||||
|
|
||||||
|
# set alternative experiences from parameters
|
||||||
|
sp_toyota_auto_brake_hold = self.params.get_bool("ToyotaAutoHold")
|
||||||
self.CP.alternativeExperience = 0
|
self.CP.alternativeExperience = 0
|
||||||
|
if sp_toyota_auto_brake_hold:
|
||||||
|
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)
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class DesireHelper:
|
|||||||
def get_lane_change_direction(CS):
|
def get_lane_change_direction(CS):
|
||||||
return LaneChangeDirection.left if CS.leftBlinker else LaneChangeDirection.right
|
return LaneChangeDirection.left if CS.leftBlinker else LaneChangeDirection.right
|
||||||
|
|
||||||
def update(self, carstate, lateral_active, lane_change_prob):
|
def update(self, carstate, lateral_active, lane_change_prob, left_edge_detected=False, right_edge_detected=False):
|
||||||
self.alc.update_params()
|
self.alc.update_params()
|
||||||
self.lane_turn_controller.update_params()
|
self.lane_turn_controller.update_params()
|
||||||
v_ego = carstate.vEgo
|
v_ego = carstate.vEgo
|
||||||
@@ -88,8 +88,8 @@ class DesireHelper:
|
|||||||
((carstate.steeringTorque > 0 and self.lane_change_direction == LaneChangeDirection.left) or
|
((carstate.steeringTorque > 0 and self.lane_change_direction == LaneChangeDirection.left) or
|
||||||
(carstate.steeringTorque < 0 and self.lane_change_direction == LaneChangeDirection.right))
|
(carstate.steeringTorque < 0 and self.lane_change_direction == LaneChangeDirection.right))
|
||||||
|
|
||||||
blindspot_detected = ((carstate.leftBlindspot and self.lane_change_direction == LaneChangeDirection.left) or
|
blindspot_detected = (((carstate.leftBlindspot or left_edge_detected) and self.lane_change_direction == LaneChangeDirection.left) or
|
||||||
(carstate.rightBlindspot and self.lane_change_direction == LaneChangeDirection.right))
|
((carstate.rightBlindspot or right_edge_detected) and self.lane_change_direction == LaneChangeDirection.right))
|
||||||
|
|
||||||
self.alc.update_lane_change(blindspot_detected, carstate.brakePressed)
|
self.alc.update_lane_change(blindspot_detected, carstate.brakePressed)
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
|||||||
# No change cost when user is controlling the speed, or when standstill
|
# No change cost when user is controlling the speed, or when standstill
|
||||||
prev_accel_constraint = not (reset_state or sm['carState'].standstill)
|
prev_accel_constraint = not (reset_state or sm['carState'].standstill)
|
||||||
|
|
||||||
accel_clip = [ACCEL_MIN, get_max_accel(v_ego)]
|
accel_clip = [ACCEL_MIN, self.accel.get_max_accel(v_ego)]
|
||||||
steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg
|
steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg
|
||||||
accel_clip = limit_accel_in_turns(v_ego, steer_angle_without_offset, accel_clip, self.CP)
|
accel_clip = limit_accel_in_turns(v_ego, steer_angle_without_offset, accel_clip, self.CP)
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ 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.a_desired)
|
self.mpc.set_cur_state(self.v_desired_filter.x, self.a_desired)
|
||||||
self.mpc.update(sm['radarState'], v_cruise, personality=sm['selfdriveState'].personality)
|
self.mpc.update(self.smooth_radarstate(sm['radarState']), v_cruise, personality=sm['selfdriveState'].personality)
|
||||||
|
|
||||||
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)
|
||||||
@@ -160,7 +160,8 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
|||||||
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
|
||||||
|
|
||||||
if self.is_e2e(sm):
|
is_e2e = self.is_e2e(sm)
|
||||||
|
if is_e2e:
|
||||||
output_a_target = min(output_a_target_e2e, output_a_target_mpc)
|
output_a_target = min(output_a_target_e2e, output_a_target_mpc)
|
||||||
self.output_should_stop = output_should_stop_e2e or output_should_stop_mpc
|
self.output_should_stop = output_should_stop_e2e or output_should_stop_mpc
|
||||||
if output_a_target < output_a_target_mpc:
|
if output_a_target < output_a_target_mpc:
|
||||||
@@ -169,8 +170,15 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
|||||||
output_a_target = output_a_target_mpc
|
output_a_target = output_a_target_mpc
|
||||||
self.output_should_stop = output_should_stop_mpc
|
self.output_should_stop = output_should_stop_mpc
|
||||||
|
|
||||||
for idx in range(2):
|
# Acceleration Personality: early soft braking (never weaker than the plan). No-op when disabled.
|
||||||
accel_clip[idx] = np.clip(accel_clip[idx], self.prev_accel_clip[idx] - 0.05, self.prev_accel_clip[idx] + 0.05)
|
output_a_target = self.accel.smooth_target_accel(output_a_target, self.a_desired_trajectory, CONTROL_N_T_IDX,
|
||||||
|
self.output_should_stop or force_slow_decel, reset=reset_state, stock_brake=is_e2e,
|
||||||
|
speed_trajectory=self.v_desired_trajectory)
|
||||||
|
|
||||||
|
# Lower (braking) bound and the ceiling's downward slew stay at the stock rate; only the ceiling's
|
||||||
|
# upward slew is tier-dependent (Acceleration Personality).
|
||||||
|
accel_clip[0] = np.clip(accel_clip[0], self.prev_accel_clip[0] - 0.05, self.prev_accel_clip[0] + 0.05)
|
||||||
|
accel_clip[1] = np.clip(accel_clip[1], self.prev_accel_clip[1] - 0.05, self.prev_accel_clip[1] + self.accel.get_rise_rate())
|
||||||
self.output_a_target = np.clip(output_a_target, accel_clip[0], accel_clip[1])
|
self.output_a_target = np.clip(output_a_target, accel_clip[0], accel_clip[1])
|
||||||
self.prev_accel_clip = accel_clip
|
self.prev_accel_clip = accel_clip
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ from openpilot.selfdrive.modeld.helpers import usbgpu_present, modeld_pkl_path,
|
|||||||
|
|
||||||
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
||||||
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
|
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
|
||||||
|
from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController
|
||||||
|
|
||||||
PROCESS_NAME = "selfdrive.modeld.modeld"
|
PROCESS_NAME = "selfdrive.modeld.modeld"
|
||||||
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
|
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
|
||||||
@@ -223,6 +224,7 @@ def main(demo=False):
|
|||||||
prev_action = log.ModelDataV2.Action()
|
prev_action = log.ModelDataV2.Action()
|
||||||
|
|
||||||
DH = DesireHelper()
|
DH = DesireHelper()
|
||||||
|
RELC = RoadEdgeLaneChangeController(DH)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
# Keep receiving frames until we are at least 1 frame ahead of previous extra frame
|
# Keep receiving frames until we are at least 1 frame ahead of previous extra frame
|
||||||
@@ -325,7 +327,10 @@ def main(demo=False):
|
|||||||
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
|
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
|
||||||
r_lane_change_prob = desire_state[log.Desire.laneChangeRight]
|
r_lane_change_prob = desire_state[log.Desire.laneChangeRight]
|
||||||
lane_change_prob = l_lane_change_prob + r_lane_change_prob
|
lane_change_prob = l_lane_change_prob + r_lane_change_prob
|
||||||
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
|
RELC.update(modelv2_send.modelV2.roadEdgeStds, modelv2_send.modelV2.laneLineProbs, v_ego, modelv2_send.modelV2.roadEdges)
|
||||||
|
mdv2sp_send.modelDataV2SP.leftLaneChangeEdgeBlock = RELC.left_edge_detected
|
||||||
|
mdv2sp_send.modelDataV2SP.rightLaneChangeEdgeBlock = RELC.right_edge_detected
|
||||||
|
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob, RELC.left_edge_detected, RELC.right_edge_detected)
|
||||||
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
|
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
|
||||||
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
|
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
|
||||||
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
|
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
|
||||||
|
|||||||
@@ -321,9 +321,16 @@ class SelfdriveD(CruiseHelper):
|
|||||||
# Handle lane change
|
# Handle lane change
|
||||||
if self.sm['modelV2'].meta.laneChangeState == LaneChangeState.preLaneChange:
|
if self.sm['modelV2'].meta.laneChangeState == LaneChangeState.preLaneChange:
|
||||||
direction = self.sm['modelV2'].meta.laneChangeDirection
|
direction = self.sm['modelV2'].meta.laneChangeDirection
|
||||||
|
mdv2sp = self.sm['modelDataV2SP']
|
||||||
|
|
||||||
if (CS.leftBlindspot and direction == LaneChangeDirection.left) or \
|
if (CS.leftBlindspot and direction == LaneChangeDirection.left) or \
|
||||||
(CS.rightBlindspot and direction == LaneChangeDirection.right):
|
(CS.rightBlindspot and direction == LaneChangeDirection.right):
|
||||||
self.events.add(EventName.laneChangeBlocked)
|
self.events.add(EventName.laneChangeBlocked)
|
||||||
|
|
||||||
|
elif (mdv2sp.leftLaneChangeEdgeBlock and direction == LaneChangeDirection.left) or \
|
||||||
|
(mdv2sp.rightLaneChangeEdgeBlock and direction == LaneChangeDirection.right):
|
||||||
|
self.events_sp.add(custom.OnroadEventSP.EventName.laneChangeRoadEdge)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if direction == LaneChangeDirection.left:
|
if direction == LaneChangeDirection.left:
|
||||||
self.events.add(EventName.preLaneChangeLeft)
|
self.events.add(EventName.preLaneChangeLeft)
|
||||||
|
|||||||
@@ -31,6 +31,15 @@ DESCRIPTIONS = {
|
|||||||
"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)."
|
||||||
),
|
),
|
||||||
|
"AccelPersonalityEnabled": tr_noop("Enable Eco/Normal/Sport acceleration profiles, including early soft braking."),
|
||||||
|
"AccelPersonality": tr_noop(
|
||||||
|
"Eco accelerates gently and brakes early and soft; Sport accelerates briskly. " +
|
||||||
|
"Hard-braking authority is always preserved."
|
||||||
|
),
|
||||||
|
"RadarDistance": tr_noop(
|
||||||
|
"Hold a lead through brief radar flicker/dropout so sunnypilot does not lose and re-grab it, " +
|
||||||
|
"smoothing the hard/late brakes that radar drop-outs cause. Braking is never reduced below stock."
|
||||||
|
),
|
||||||
"AlwaysOnDM": tr_noop("Enable driver monitoring even when sunnypilot is not engaged."),
|
"AlwaysOnDM": tr_noop("Enable driver monitoring even when sunnypilot is not engaged."),
|
||||||
'RecordFront': tr_noop("Upload data from the driver facing camera and help improve the driver monitoring algorithm."),
|
'RecordFront': tr_noop("Upload data from the driver facing camera and help improve the driver monitoring algorithm."),
|
||||||
"IsMetric": tr_noop("Display speed in km/h instead of mph."),
|
"IsMetric": tr_noop("Display speed in km/h instead of mph."),
|
||||||
@@ -64,6 +73,12 @@ class TogglesLayout(Widget):
|
|||||||
"disengage_on_accelerator.png",
|
"disengage_on_accelerator.png",
|
||||||
False,
|
False,
|
||||||
),
|
),
|
||||||
|
"RadarDistance": (
|
||||||
|
lambda: tr("Radar Distance"),
|
||||||
|
DESCRIPTIONS["RadarDistance"],
|
||||||
|
"speed_limit.png",
|
||||||
|
False,
|
||||||
|
),
|
||||||
"IsLdwEnabled": (
|
"IsLdwEnabled": (
|
||||||
lambda: tr("Enable Lane Departure Warnings"),
|
lambda: tr("Enable Lane Departure Warnings"),
|
||||||
DESCRIPTIONS["IsLdwEnabled"],
|
DESCRIPTIONS["IsLdwEnabled"],
|
||||||
@@ -106,6 +121,24 @@ class TogglesLayout(Widget):
|
|||||||
icon="speed_limit.png"
|
icon="speed_limit.png"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self._accel_personality_enabled = toggle_item(
|
||||||
|
lambda: tr("Enable Acceleration Profiles"),
|
||||||
|
lambda: tr(DESCRIPTIONS["AccelPersonalityEnabled"]),
|
||||||
|
self._params.get_bool("AccelPersonalityEnabled"),
|
||||||
|
callback=self._set_accel_personality_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():
|
||||||
@@ -135,9 +168,11 @@ class TogglesLayout(Widget):
|
|||||||
|
|
||||||
self._toggles[param] = toggle
|
self._toggles[param] = toggle
|
||||||
|
|
||||||
# insert longitudinal personality after NDOG toggle
|
# insert longitudinal + acceleration 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_personality_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)
|
||||||
@@ -176,11 +211,15 @@ 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_personality_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_personality_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.")
|
||||||
@@ -247,3 +286,9 @@ 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_personality_enabled(self, state: bool):
|
||||||
|
self._params.put_bool("AccelPersonalityEnabled", state, block=True)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ 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.onroad import OnroadViewContainerSP as AugmentedRoadView
|
||||||
|
|
||||||
ONROAD_DELAY = 2.5 # seconds
|
ONROAD_DELAY = 2.5 # seconds
|
||||||
|
|
||||||
@@ -118,13 +119,15 @@ 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:
|
||||||
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
|
if not gui_app.sunnypilot_ui() or self._should_auto_scroll_to_onroad():
|
||||||
|
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
|
||||||
self._onroad_time_delay = None
|
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:
|
||||||
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
|
if not gui_app.sunnypilot_ui() or self._should_auto_scroll_to_onroad():
|
||||||
|
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
|
||||||
self._prev_standstill = CS.standstill
|
self._prev_standstill = CS.standstill
|
||||||
|
|
||||||
def _on_interactive_timeout(self):
|
def _on_interactive_timeout(self):
|
||||||
|
|||||||
@@ -51,11 +51,17 @@ class LaneChangeSettingsLayout(Widget):
|
|||||||
description=lambda: tr("Toggle to enable a delay timer for seamless lane changes when blind spot monitoring " +
|
description=lambda: tr("Toggle to enable a delay timer for seamless lane changes when blind spot monitoring " +
|
||||||
"(BSM) detects a obstructing vehicle, ensuring safe maneuvering."),
|
"(BSM) detects a obstructing vehicle, ensuring safe maneuvering."),
|
||||||
)
|
)
|
||||||
|
self._road_edge_block = toggle_item_sp(
|
||||||
|
param="RoadEdgeLaneChangeEnabled",
|
||||||
|
title=lambda: tr("Block Lane Change: Road Edge Detection"),
|
||||||
|
description=lambda: tr("Blocks the lane change if the model sees a road edge on your signaled side."),
|
||||||
|
)
|
||||||
|
|
||||||
items = [
|
items = [
|
||||||
self._lane_change_timer,
|
self._lane_change_timer,
|
||||||
LineSeparatorSP(40),
|
LineSeparatorSP(40),
|
||||||
self._bsm_delay,
|
self._bsm_delay,
|
||||||
|
self._road_edge_block,
|
||||||
]
|
]
|
||||||
|
|
||||||
return items
|
return items
|
||||||
|
|||||||
@@ -0,0 +1,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.
|
||||||
|
See the LICENSE.md file in the root directory for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout
|
||||||
|
|
||||||
|
|
||||||
|
class MiciMainLayoutSP(MiciMainLayout):
|
||||||
|
def _should_auto_scroll_to_onroad(self) -> bool:
|
||||||
|
return not self._onroad_layout.is_on_info_panel()
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
"""
|
||||||
|
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 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, callback) -> None:
|
||||||
|
self.road_view.set_click_callback(callback)
|
||||||
|
self.onroad_info_panel.set_click_callback(callback)
|
||||||
|
|
||||||
|
def is_on_info_panel(self) -> bool:
|
||||||
|
"""True when scrolled past halfway toward onroad_info_panel (used by main layout
|
||||||
|
to skip auto-pop-back-to-camera while user is reading the info panel)."""
|
||||||
|
return abs(self._scroller.scroll_panel.get_offset()) > self._rect.height / 2
|
||||||
|
|
||||||
|
def _render(self, rect: rl.Rectangle):
|
||||||
|
if abs(self.rect.x) > gui_app.width * HORIZONTAL_RESET_RATIO:
|
||||||
|
self._scroller.scroll_panel.set_offset(0)
|
||||||
|
|
||||||
|
vertical_offset = self._scroller.scroll_panel.get_offset()
|
||||||
|
show_ball = abs(vertical_offset) < rect.height * CONFIDENCE_BALL_VISIBLE_RATIO
|
||||||
|
self.road_view.set_show_confidence_ball(show_ball)
|
||||||
|
|
||||||
|
super()._render(rect)
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
"""
|
||||||
|
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
|
||||||
|
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
|
||||||
|
from openpilot.system.ui.lib.multilang import tr
|
||||||
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||||
|
from openpilot.system.ui.lib.application import MousePos
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class OnroadInfoPanelColors:
|
||||||
|
white: rl.Color = rl.WHITE
|
||||||
|
black: rl.Color = rl.BLACK
|
||||||
|
red: rl.Color = rl.Color(255, 0, 0, 255)
|
||||||
|
green: rl.Color = rl.Color(0, 255, 0, 255)
|
||||||
|
grey: rl.Color = rl.Color(190, 195, 190, 255)
|
||||||
|
light_grey: rl.Color = rl.Color(200, 200, 200, 255)
|
||||||
|
dark_grey: rl.Color = rl.Color(100, 100, 100, 255)
|
||||||
|
bg_dark: rl.Color = rl.Color(0, 0, 0, 255)
|
||||||
|
card_bg: rl.Color = rl.Color(50, 50, 50, 200)
|
||||||
|
badge_bg: rl.Color = 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)
|
||||||
|
margin = 20
|
||||||
|
mid_y = rect.y + rect.height / 2
|
||||||
|
|
||||||
|
left_x = rect.x + 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
|
||||||
|
|
||||||
|
speed_val = str(round(display_speed))
|
||||||
|
if self.speed_limit_valid and display_speed > self.speed_limit:
|
||||||
|
speed_color = COLORS.red
|
||||||
|
else:
|
||||||
|
speed_color = COLORS.white
|
||||||
|
|
||||||
|
rl.draw_text_ex(self._font_semi_bold, unit, rl.Vector2(left_x, mid_y - 95), 38, 0, COLORS.grey)
|
||||||
|
rl.draw_text_ex(self._font_bold, speed_val, rl.Vector2(left_x, mid_y - 60), 110, 0, speed_color)
|
||||||
|
|
||||||
|
sign_width = 135
|
||||||
|
sign_height = 135 if ui_state.is_metric else 175
|
||||||
|
|
||||||
|
has_next = self.next_speed_limit > 0 and self.next_speed_limit != self.speed_limit
|
||||||
|
target_slide = 1.0 if has_next else 0.0
|
||||||
|
slide_speed = 3.0 * rl.get_frame_time()
|
||||||
|
if self._sign_slide < target_slide:
|
||||||
|
self._sign_slide = min(self._sign_slide + slide_speed, target_slide)
|
||||||
|
elif self._sign_slide > target_slide:
|
||||||
|
self._sign_slide = max(self._sign_slide - slide_speed, target_slide)
|
||||||
|
|
||||||
|
next_w = int(sign_width * 0.7)
|
||||||
|
next_h = int(sign_height * 0.7)
|
||||||
|
next_peek = int(next_w * 0.85) + 5
|
||||||
|
centered_x = rect.x + rect.width - sign_width - margin
|
||||||
|
shifted_x = rect.x + rect.width - sign_width - margin - next_peek
|
||||||
|
sign_x = centered_x + (shifted_x - centered_x) * self._sign_slide
|
||||||
|
sign_y = rect.y + (rect.height - sign_height) / 2
|
||||||
|
|
||||||
|
road_y = mid_y + 55
|
||||||
|
road_width = sign_x - left_x - margin
|
||||||
|
self._draw_road_name(left_x, road_y, road_width)
|
||||||
|
|
||||||
|
if has_next and self._sign_slide > 0.01:
|
||||||
|
next_val = str(round(self.next_speed_limit))
|
||||||
|
dist_str = self._format_distance(self.next_speed_limit_distance)
|
||||||
|
next_x = sign_x + sign_width - int(next_w * 0.15)
|
||||||
|
next_y = sign_y + (sign_height - next_h) / 2
|
||||||
|
|
||||||
|
next_speed_color = COLORS.black
|
||||||
|
if ui_state.is_metric:
|
||||||
|
self._draw_vienna_sign(next_x, next_y, next_w, next_h, next_val, next_speed_color, is_upcoming=True)
|
||||||
|
else:
|
||||||
|
self._draw_mutcd_sign(next_x, next_y, next_w, next_h, next_val, next_speed_color, is_upcoming=True)
|
||||||
|
|
||||||
|
dist_size = measure_text_cached(self._font_medium, dist_str, 24)
|
||||||
|
rl.draw_text_ex(self._font_medium, dist_str, rl.Vector2(next_x + next_w / 2 - dist_size.x / 2, next_y + next_h + 4), 24, 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_val = str(abs(round(self.speed_limit_offset)))
|
||||||
|
badge_sz = 42
|
||||||
|
badge_x = sign_x + sign_width - badge_sz * 0.85
|
||||||
|
badge_y = sign_y - badge_sz * 0.25
|
||||||
|
|
||||||
|
if ui_state.is_metric:
|
||||||
|
badge_r = badge_sz / 2
|
||||||
|
badge_cx = badge_x + badge_r
|
||||||
|
badge_cy = badge_y + badge_r
|
||||||
|
rl.draw_circle(int(badge_cx), int(badge_cy), badge_r + 2, COLORS.dark_grey)
|
||||||
|
rl.draw_circle(int(badge_cx), int(badge_cy), badge_r, COLORS.badge_bg)
|
||||||
|
self._draw_text_centered(self._font_bold, offset_val, 24, rl.Vector2(badge_cx, badge_cy), COLORS.white)
|
||||||
|
else:
|
||||||
|
mutcd_badge_x = sign_x + sign_width - badge_sz * 0.65
|
||||||
|
mutcd_badge_y = sign_y - badge_sz * 0.50
|
||||||
|
badge_rect = rl.Rectangle(mutcd_badge_x, mutcd_badge_y, badge_sz, badge_sz)
|
||||||
|
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(self._font_bold, offset_val, 24, rl.Vector2(mutcd_badge_x + badge_sz / 2, mutcd_badge_y + badge_sz / 2), COLORS.white)
|
||||||
|
|
||||||
|
# SCC
|
||||||
|
speed_size = measure_text_cached(self._font_bold, speed_val, 110)
|
||||||
|
scc_x = left_x + speed_size.x + 30
|
||||||
|
scc_y = mid_y - 50
|
||||||
|
self._draw_scc_icons(scc_x, scc_y)
|
||||||
|
|
||||||
|
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) -> None:
|
||||||
|
sm = ui_state.sm
|
||||||
|
if not sm.valid["longitudinalPlanSP"]:
|
||||||
|
return
|
||||||
|
scc = sm["longitudinalPlanSP"].smartCruiseControl
|
||||||
|
|
||||||
|
box_w, box_h = 100, 36
|
||||||
|
gap = 6
|
||||||
|
drawn = 0
|
||||||
|
|
||||||
|
for label, active in [("SCC-V", scc.vision.active), ("SCC-M", scc.map.active)]:
|
||||||
|
if not active:
|
||||||
|
continue
|
||||||
|
bx = x
|
||||||
|
by = y + drawn * (box_h + gap)
|
||||||
|
rl.draw_rectangle_rounded(rl.Rectangle(bx, by, box_w, box_h), 0.3, 10, COLORS.green)
|
||||||
|
self._draw_text_centered(self._font_bold, label, 20, rl.Vector2(bx + box_w / 2, by + box_h / 2), COLORS.black)
|
||||||
|
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:
|
||||||
|
road_display = self.road_name if self.road_name else "--"
|
||||||
|
font_size = 30
|
||||||
|
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)
|
||||||
|
text_size = measure_text_cached(self._font_bold, speed_str, int(font_size))
|
||||||
|
text_pos = rl.Vector2(center.x - text_size.x / 2, center.y - text_size.y / 2)
|
||||||
|
rl.draw_text_ex(self._font_bold, speed_str, text_pos, font_size, 0, speed_color)
|
||||||
|
|
||||||
|
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(self._font_bold, tr("AHEAD"), label_size, rl.Vector2(mid_x, y + height * 0.27), COLORS.black)
|
||||||
|
else:
|
||||||
|
self._draw_text_centered(self._font_bold, tr("SPEED"), label_size, rl.Vector2(mid_x, y + height * 0.20), COLORS.black)
|
||||||
|
self._draw_text_centered(self._font_bold, tr("LIMIT"), label_size, rl.Vector2(mid_x, y + height * 0.40), COLORS.black)
|
||||||
|
|
||||||
|
speed_font_size = int(width * 0.52) if len(speed_str) >= 3 else int(width * 0.62)
|
||||||
|
self._draw_text_centered(self._font_bold, speed_str, speed_font_size, rl.Vector2(mid_x, y + height * 0.72), speed_color)
|
||||||
|
|
||||||
|
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 _format_distance(self, distance: float) -> str:
|
||||||
|
if ui_state.is_metric:
|
||||||
|
if distance < 50:
|
||||||
|
return tr("Near")
|
||||||
|
if distance >= 1000:
|
||||||
|
return f"{distance * METER_TO_KM:.1f}" + tr("km")
|
||||||
|
if distance < 200:
|
||||||
|
rounded = max(10, int(distance / 10) * 10)
|
||||||
|
else:
|
||||||
|
rounded = int(distance / 100) * 100
|
||||||
|
return str(rounded) + tr("m")
|
||||||
|
else:
|
||||||
|
distance_mi = distance * METER_TO_MILE
|
||||||
|
if distance_mi < 0.1:
|
||||||
|
return tr("Near")
|
||||||
|
return f"{distance_mi:.1f}" + tr("mi")
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"""
|
||||||
|
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.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, rect: rl.Rectangle) -> None:
|
||||||
|
super()._render(rect)
|
||||||
|
if self._show_confidence_ball:
|
||||||
|
self._real_confidence_ball.render(self.rect)
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""
|
||||||
|
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):
|
||||||
|
"""Reject orthogonal-dominant drags so nested scrollers (outer horizontal +
|
||||||
|
inner vertical) don't both engage on a slightly diagonal swipe.
|
||||||
|
|
||||||
|
Implemented as a post-super state rollback rather than reimplementing the
|
||||||
|
PRESSED state machine — keeps stock behaviour authoritative."""
|
||||||
|
|
||||||
|
def _handle_mouse_event(self, mouse_event: MouseEvent, bounds: rl.Rectangle, bounds_size: float,
|
||||||
|
content_size: float) -> None:
|
||||||
|
pre_state = self._state
|
||||||
|
super()._handle_mouse_event(mouse_event, bounds, bounds_size, content_size)
|
||||||
|
|
||||||
|
if self._state == ScrollState.MANUAL_SCROLL and pre_state == ScrollState.PRESSED and \
|
||||||
|
self._initial_click_event is not None:
|
||||||
|
diff_x = abs(mouse_event.pos.x - self._initial_click_event.pos.x)
|
||||||
|
diff_y = abs(mouse_event.pos.y - self._initial_click_event.pos.y)
|
||||||
|
along = diff_x if self._horizontal else diff_y
|
||||||
|
anti = diff_y if self._horizontal else diff_x
|
||||||
|
if anti > along:
|
||||||
|
self._state = ScrollState.STEADY
|
||||||
|
self._velocity = 0.0
|
||||||
|
self._velocity_buffer.clear()
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||||
|
|
||||||
|
This file is part of sunnypilot and is licensed under the MIT License.
|
||||||
|
See the LICENSE.md file in the root directory for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from openpilot.system.ui.widgets.scroller import Scroller
|
||||||
|
from openpilot.selfdrive.ui.sunnypilot.mici.widgets.scroll_panel_sp import GuiScrollPanel2SP
|
||||||
|
|
||||||
|
|
||||||
|
class ScrollerSP(Scroller):
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
inner = self._scroller
|
||||||
|
inner.scroll_panel = GuiScrollPanel2SP(inner._horizontal, handle_out_of_bounds=not inner._snap_items)
|
||||||
@@ -10,6 +10,9 @@ from openpilot.selfdrive.ui.layouts.main import MainLayout
|
|||||||
from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout
|
from openpilot.selfdrive.ui.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()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelp
|
|||||||
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
||||||
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
|
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
|
||||||
from openpilot.sunnypilot.models.helpers import get_active_bundle
|
from openpilot.sunnypilot.models.helpers import get_active_bundle
|
||||||
|
from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController
|
||||||
|
|
||||||
PROCESS_NAME = "selfdrive.modeld.modeld_tinygrad"
|
PROCESS_NAME = "selfdrive.modeld.modeld_tinygrad"
|
||||||
|
|
||||||
@@ -329,6 +330,7 @@ def main(demo=False):
|
|||||||
prev_action = log.ModelDataV2.Action()
|
prev_action = log.ModelDataV2.Action()
|
||||||
|
|
||||||
DH = DesireHelper()
|
DH = DesireHelper()
|
||||||
|
RELC = RoadEdgeLaneChangeController(DH)
|
||||||
meta_constants = load_meta_constants()
|
meta_constants = load_meta_constants()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
@@ -433,7 +435,10 @@ def main(demo=False):
|
|||||||
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
|
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
|
||||||
r_lane_change_prob = desire_state[log.Desire.laneChangeRight]
|
r_lane_change_prob = desire_state[log.Desire.laneChangeRight]
|
||||||
lane_change_prob = l_lane_change_prob + r_lane_change_prob
|
lane_change_prob = l_lane_change_prob + r_lane_change_prob
|
||||||
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
|
RELC.update(modelv2_send.modelV2.roadEdgeStds, modelv2_send.modelV2.laneLineProbs, v_ego, modelv2_send.modelV2.roadEdges)
|
||||||
|
mdv2sp_send.modelDataV2SP.leftLaneChangeEdgeBlock = RELC.left_edge_detected
|
||||||
|
mdv2sp_send.modelDataV2SP.rightLaneChangeEdgeBlock = RELC.right_edge_detected
|
||||||
|
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob, RELC.left_edge_detected, RELC.right_edge_detected)
|
||||||
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
|
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
|
||||||
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
|
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
|
||||||
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
|
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
|
||||||
|
|||||||
@@ -0,0 +1,246 @@
|
|||||||
|
"""
|
||||||
|
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 Sequence
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from cereal import messaging
|
||||||
|
from opendbc.car import structs
|
||||||
|
from openpilot.common.params import Params
|
||||||
|
from openpilot.common.realtime import DT_MDL
|
||||||
|
from openpilot.sunnypilot import get_sanitize_int_param
|
||||||
|
from openpilot.sunnypilot.selfdrive.controls.lib.accel_personality.constants import \
|
||||||
|
NORMAL, PERSONALITY_MIN, PERSONALITY_MAX, A_CRUISE_MAX_BP, A_CRUISE_MAX_V, RISE_RATE, SMOOTH_DECEL_BP, \
|
||||||
|
SMOOTH_DECEL_V, BRAKE_DEEPENING_JERK, BRAKE_RELEASE_JERK, ACCEL_RISE_JERK, SMOOTH_DECEL_LOOKAHEAD_T, \
|
||||||
|
MIN_SMOOTH_BRAKE_NEED, HARD_BRAKE_TARGET_ACCEL, HARD_BRAKE_NEED, STOP_IMMINENT_VEGO, STOP_IMMINENT_LOOKAHEAD_T, \
|
||||||
|
ONSET_JERK0, ONSET_JERK_GAIN, ONSET_GAP_SOFT, ONSET_GAP_GAIN, ONSET_JERK_MAX, ONSET_HANDBACK_JERK, \
|
||||||
|
SOFT_ONSET_MAX_BRAKE_NEED, SOFT_ONSET_MAX_INSTANT_ACCEL, SOFT_ONSET_REARM_FRAMES
|
||||||
|
|
||||||
|
_ZERO_ACCEL_EPS = 1e-6
|
||||||
|
|
||||||
|
|
||||||
|
class AccelController:
|
||||||
|
def __init__(self, CP: structs.CarParams, mpc, params=None):
|
||||||
|
self._CP = CP
|
||||||
|
self._mpc = mpc
|
||||||
|
self._params = params or Params()
|
||||||
|
self._frame = 0
|
||||||
|
self._enabled = self._params.get_bool("AccelPersonalityEnabled")
|
||||||
|
self._personality = NORMAL
|
||||||
|
self._v_ego = 0.0
|
||||||
|
self._last_target_accel = 0.0
|
||||||
|
self._brake_need = 0.0
|
||||||
|
self._decel_target = 0.0
|
||||||
|
self._smooth_active = False
|
||||||
|
self._bypassed = False
|
||||||
|
# convex brake-onset shaper state
|
||||||
|
self._onset_latched = False # sticky: True once an onset goes firm -> no re-soften until sustained release
|
||||||
|
self._onset_release = 0 # consecutive non-deepening frames (sticky re-arm debounce)
|
||||||
|
self._soft_active = False # True iff the convex shaper governed this frame's output (bypasses min(.,raw))
|
||||||
|
self._soft_episode = False # True while a soft onset is open (incl. closing its gap) -> own deepening, no snap
|
||||||
|
self._read_params()
|
||||||
|
|
||||||
|
def _read_params(self) -> None:
|
||||||
|
self._enabled = self._params.get_bool("AccelPersonalityEnabled")
|
||||||
|
if not self._enabled:
|
||||||
|
self._personality = NORMAL
|
||||||
|
return
|
||||||
|
self._personality = get_sanitize_int_param("AccelPersonality", PERSONALITY_MIN, PERSONALITY_MAX, self._params)
|
||||||
|
|
||||||
|
def update(self, sm: messaging.SubMaster) -> None:
|
||||||
|
if self._frame % int(1. / DT_MDL) == 0:
|
||||||
|
self._read_params()
|
||||||
|
self._v_ego = sm['carState'].vEgo
|
||||||
|
self._frame += 1
|
||||||
|
|
||||||
|
def get_max_accel(self, v_ego: float) -> float:
|
||||||
|
return float(np.interp(v_ego, A_CRUISE_MAX_BP, A_CRUISE_MAX_V[self._personality]))
|
||||||
|
|
||||||
|
def get_rise_rate(self) -> float:
|
||||||
|
return RISE_RATE[self._personality]
|
||||||
|
|
||||||
|
def get_decel_target(self, brake_need: float) -> float:
|
||||||
|
return float(np.interp(max(0.0, float(brake_need)), SMOOTH_DECEL_BP, SMOOTH_DECEL_V[self._personality]))
|
||||||
|
|
||||||
|
def smooth_target_accel(self, raw_target_accel: float, accel_trajectory: Sequence[float], t_idxs: Sequence[float],
|
||||||
|
should_stop: bool, reset: bool = False, stock_brake: bool = False,
|
||||||
|
speed_trajectory: Sequence[float] | None = None) -> float:
|
||||||
|
raw = float(raw_target_accel)
|
||||||
|
self._brake_need = self._compute_brake_need(raw, accel_trajectory, t_idxs)
|
||||||
|
self._decel_target = 0.0
|
||||||
|
self._smooth_active = False
|
||||||
|
self._soft_active = False
|
||||||
|
|
||||||
|
# The convex onset shaper runs ONLY for ECO/SPORT (NORMAL and disabled are stock). Reset its state
|
||||||
|
# whenever it cannot run so nothing leaks across a personality toggle or a passthrough interlude.
|
||||||
|
if not (self._enabled and self._personality != NORMAL):
|
||||||
|
self._reset_onset()
|
||||||
|
|
||||||
|
# Passthroughs (hand the plan straight through, no shaping):
|
||||||
|
if reset or not self._enabled or (stock_brake and (raw < 0.0 or self._brake_need >= MIN_SMOOTH_BRAKE_NEED)):
|
||||||
|
self._bypassed = False # disabled / reset / blended-e2e braking
|
||||||
|
return self._passthrough(raw)
|
||||||
|
self._bypassed = self._emergency_bypass(raw, should_stop)
|
||||||
|
if self._bypassed: # a hard brake must never be softened
|
||||||
|
return self._stand_down(raw)
|
||||||
|
if self._stop_imminent(speed_trajectory, t_idxs): # stop coming -> stock decel, no coast/creep
|
||||||
|
return self._stand_down(raw)
|
||||||
|
|
||||||
|
# Front-load a gentle early brake when a deeper brake is predicted ahead. The convex shaper owns the
|
||||||
|
# output when it governed this frame (soft_active); otherwise never weaker than the plan.
|
||||||
|
if self._brake_need >= MIN_SMOOTH_BRAKE_NEED:
|
||||||
|
self._smooth_active = True
|
||||||
|
self._decel_target = self.get_decel_target(self._brake_need)
|
||||||
|
slewed = self._slew(min(raw, self._decel_target))
|
||||||
|
return self._finalize(slewed if self._soft_active else min(slewed, raw))
|
||||||
|
|
||||||
|
# Below the smooth-brake threshold: track the plan, never weaker than it while braking.
|
||||||
|
slewed = self._slew(raw)
|
||||||
|
if self._soft_active or raw >= 0.0:
|
||||||
|
return self._finalize(slewed)
|
||||||
|
return self._finalize(min(slewed, raw))
|
||||||
|
|
||||||
|
def _stop_imminent(self, speed_trajectory: Sequence[float] | None, t_idxs: Sequence[float]) -> bool:
|
||||||
|
# plan predicts a near-stop within the lookahead -> a stop is coming (lead or light/sign).
|
||||||
|
if speed_trajectory is None:
|
||||||
|
return False
|
||||||
|
return any(float(s) < STOP_IMMINENT_VEGO
|
||||||
|
for s, t in zip(speed_trajectory, t_idxs, strict=False) if float(t) <= STOP_IMMINENT_LOOKAHEAD_T)
|
||||||
|
|
||||||
|
def _compute_brake_need(self, raw_target_accel: float, accel_trajectory: Sequence[float], t_idxs: Sequence[float]) -> float:
|
||||||
|
min_accel = float(raw_target_accel)
|
||||||
|
for accel, t in zip(accel_trajectory, t_idxs, strict=False):
|
||||||
|
if float(t) <= SMOOTH_DECEL_LOOKAHEAD_T:
|
||||||
|
min_accel = min(min_accel, float(accel))
|
||||||
|
return max(0.0, -min_accel)
|
||||||
|
|
||||||
|
def _emergency_bypass(self, raw_target_accel: float, should_stop: bool) -> bool:
|
||||||
|
return (self._mpc.crash_cnt > 0 or should_stop or
|
||||||
|
raw_target_accel <= HARD_BRAKE_TARGET_ACCEL or self._brake_need >= HARD_BRAKE_NEED)
|
||||||
|
|
||||||
|
def _slew(self, target_accel: float) -> float:
|
||||||
|
target_accel = float(target_accel)
|
||||||
|
p = self._personality
|
||||||
|
jmax = BRAKE_DEEPENING_JERK[p]
|
||||||
|
deepening = target_accel <= self._last_target_accel
|
||||||
|
if not deepening:
|
||||||
|
# genuine release / coast: close any soft episode, advance the re-arm debounce, unlatch after a
|
||||||
|
# sustained release.
|
||||||
|
self._soft_episode = False
|
||||||
|
self._onset_release += 1
|
||||||
|
if self._onset_release >= SOFT_ONSET_REARM_FRAMES:
|
||||||
|
self._onset_latched = False
|
||||||
|
return self._slew_up(target_accel)
|
||||||
|
self._onset_release = 0
|
||||||
|
# NORMAL (and disabled, forced to NORMAL) -> stock constant-jerk linear deepening, byte-exact.
|
||||||
|
if p == NORMAL:
|
||||||
|
return self._clean_accel(max(target_accel, self._last_target_accel - jmax * DT_MDL))
|
||||||
|
return self._slew_convex(target_accel, jmax)
|
||||||
|
|
||||||
|
def _onset_soft_armed(self, target_accel: float) -> bool:
|
||||||
|
# Gentle non-emergency onset. Armed from the FIRST deepening tick (no brake_need lower gate, so the
|
||||||
|
# gentle bite lands on the actual onset, not after the plan has already deepened). Two upper gates
|
||||||
|
# keep it off firm/deep braking: the 3s-lookahead brake_need ceiling AND the instantaneous raw depth.
|
||||||
|
return (self._enabled and self._personality != NORMAL and
|
||||||
|
0.0 < self._brake_need < SOFT_ONSET_MAX_BRAKE_NEED and
|
||||||
|
target_accel > SOFT_ONSET_MAX_INSTANT_ACCEL)
|
||||||
|
|
||||||
|
def _slew_convex(self, target_accel: float, jmax: float) -> float:
|
||||||
|
# target_accel is the effective plan to track (raw, or min(raw, decel_target) on the smooth branch).
|
||||||
|
# Dispatch: armed -> gentle bite; firm zone with an open soft gap -> fast hand-back; else stock.
|
||||||
|
last = self._last_target_accel
|
||||||
|
gap = max(0.0, last - target_accel) # m/s^2 currently shallower than the plan (last,target both <=0)
|
||||||
|
soft_armed = self._onset_soft_armed(target_accel)
|
||||||
|
if soft_armed and not self._onset_latched:
|
||||||
|
return self._onset_bite(target_accel, last, gap)
|
||||||
|
if soft_armed: # firm/deep zone -> latch off further (re)arming
|
||||||
|
self._onset_latched = True
|
||||||
|
if self._soft_episode and gap > _ZERO_ACCEL_EPS:
|
||||||
|
return self._onset_handback(target_accel)
|
||||||
|
self._soft_episode = False # no open gap: NEVER soften a fresh firm brake
|
||||||
|
return self._clean_accel(max(target_accel, last - jmax * DT_MDL)) # stock; caller does min(.,raw)
|
||||||
|
|
||||||
|
def _onset_bite(self, target_accel: float, last: float, gap: float) -> float:
|
||||||
|
# Gentle convex onset. Depth-proportional jerk: gentle ONSET_JERK0 at the bite (a~0), growing with
|
||||||
|
# current decel depth -- da/dt = j0 + k*a integrates to a(t) = (j0/k)*(exp(k*t)-1), the exponential-
|
||||||
|
# growth profile. A stateless instantaneous-gap catch-up adds bounded jerk once realized lags the plan
|
||||||
|
# by more than ONSET_GAP_SOFT, hard-capped at ONSET_JERK_MAX so even the catch is never a grab.
|
||||||
|
p = self._personality
|
||||||
|
self._soft_episode = True
|
||||||
|
jerk = ONSET_JERK0[p] + ONSET_JERK_GAIN[p] * abs(last)
|
||||||
|
jerk = min(jerk + ONSET_GAP_GAIN[p] * max(0.0, gap - ONSET_GAP_SOFT[p]), ONSET_JERK_MAX[p])
|
||||||
|
out = max(last - jerk * DT_MDL, target_accel) # never deeper than the plan -> only softer-or-equal
|
||||||
|
if out <= target_accel + _ZERO_ACCEL_EPS: # gap closed -> episode complete
|
||||||
|
self._soft_episode = False
|
||||||
|
self._soft_active = True
|
||||||
|
return self._clean_accel(out)
|
||||||
|
|
||||||
|
def _onset_handback(self, target_accel: float) -> float:
|
||||||
|
# Plan left the gentle zone but a soft gap is still open: close it FAST (firm, jerk-limited so it is
|
||||||
|
# not a snap) so the output catches the plan before braking gets firm -> no late-brake lag.
|
||||||
|
out = max(self._last_target_accel - ONSET_HANDBACK_JERK[self._personality] * DT_MDL, target_accel)
|
||||||
|
if out <= target_accel + _ZERO_ACCEL_EPS:
|
||||||
|
self._soft_episode = False
|
||||||
|
self._soft_active = True
|
||||||
|
return self._clean_accel(out)
|
||||||
|
|
||||||
|
def _reset_onset(self) -> None:
|
||||||
|
self._onset_latched = False
|
||||||
|
self._onset_release = 0
|
||||||
|
self._soft_active = False
|
||||||
|
self._soft_episode = False
|
||||||
|
|
||||||
|
def _slew_up(self, target_accel: float) -> float:
|
||||||
|
if self._last_target_accel < 0.0:
|
||||||
|
released = min(target_accel, self._last_target_accel + BRAKE_RELEASE_JERK * DT_MDL)
|
||||||
|
if released <= 0.0:
|
||||||
|
return self._clean_accel(released)
|
||||||
|
return self._clean_accel(min(target_accel, ACCEL_RISE_JERK[self._personality] * DT_MDL))
|
||||||
|
step = ACCEL_RISE_JERK[self._personality] * DT_MDL
|
||||||
|
return self._clean_accel(min(target_accel, self._last_target_accel + step))
|
||||||
|
|
||||||
|
def _passthrough(self, target_accel: float) -> float:
|
||||||
|
self._smooth_active = False
|
||||||
|
self._soft_active = False
|
||||||
|
return self._finalize(target_accel)
|
||||||
|
|
||||||
|
def _stand_down(self, target_accel: float) -> float:
|
||||||
|
# clear shaper state and hand the plan straight through (emergency / stop-imminent)
|
||||||
|
self._reset_onset()
|
||||||
|
return self._passthrough(target_accel)
|
||||||
|
|
||||||
|
def _finalize(self, target_accel: float) -> float:
|
||||||
|
target_accel = self._clean_accel(target_accel)
|
||||||
|
self._last_target_accel = target_accel
|
||||||
|
return target_accel
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _clean_accel(accel: float) -> float:
|
||||||
|
accel = float(accel)
|
||||||
|
return 0.0 if abs(accel) < _ZERO_ACCEL_EPS else accel
|
||||||
|
|
||||||
|
def enabled(self) -> bool:
|
||||||
|
return self._enabled
|
||||||
|
|
||||||
|
def personality(self):
|
||||||
|
return self._personality
|
||||||
|
|
||||||
|
def max_accel(self) -> float:
|
||||||
|
return self.get_max_accel(self._v_ego)
|
||||||
|
|
||||||
|
def brake_need(self) -> float:
|
||||||
|
return self._brake_need
|
||||||
|
|
||||||
|
def decel_target(self) -> float:
|
||||||
|
return self._decel_target
|
||||||
|
|
||||||
|
def smooth_active(self) -> bool:
|
||||||
|
return self._smooth_active
|
||||||
|
|
||||||
|
def bypassed(self) -> bool:
|
||||||
|
return self._bypassed
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
"""
|
||||||
|
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 cereal import custom
|
||||||
|
|
||||||
|
AccelerationPersonality = custom.LongitudinalPlanSP.AccelerationPersonality
|
||||||
|
ECO = AccelerationPersonality.eco
|
||||||
|
NORMAL = AccelerationPersonality.normal
|
||||||
|
SPORT = AccelerationPersonality.sport
|
||||||
|
|
||||||
|
PERSONALITY_MIN = min(AccelerationPersonality.schema.enumerants.values())
|
||||||
|
PERSONALITY_MAX = max(AccelerationPersonality.schema.enumerants.values())
|
||||||
|
|
||||||
|
# Accel ceiling. NORMAL is stock so a disabled controller (forced to NORMAL) is stock.
|
||||||
|
# This is the POSITIVE-accel upper clip + its upward slew rate. It is the launch/cruise-accel side and
|
||||||
|
# is independent of braking (which is the lower clip + the convex/SMOOTH_DECEL shaper) -- tuning it does
|
||||||
|
# NOT change the gentle-brake goals. ECO launch (v=0) matches stock + stock rise rate so take-off from
|
||||||
|
# a stop is prompt (no honking); only the 25/40 m/s cruise points stay gentle.
|
||||||
|
A_CRUISE_MAX_BP = [0., 14., 25., 40.]
|
||||||
|
STOCK_A_CRUISE_MAX_V = [1.6, 0.7, 0.2, 0.08]
|
||||||
|
STOCK_RISE_RATE = 0.05
|
||||||
|
A_CRUISE_MAX_V = {
|
||||||
|
ECO: [1.60, 0.60, 0.13, 0.05], # stock launch (v=0), gentle cruise (25/40)
|
||||||
|
NORMAL: STOCK_A_CRUISE_MAX_V,
|
||||||
|
SPORT: [1.90, 1.30, 0.60, 0.25],
|
||||||
|
}
|
||||||
|
RISE_RATE = {ECO: 0.05, NORMAL: STOCK_RISE_RATE, SPORT: 0.06} # ECO rise = stock so launch ramps promptly
|
||||||
|
|
||||||
|
# Early soft braking: predicted brake need (m/s^2) -> early decel target (m/s^2).
|
||||||
|
SMOOTH_DECEL_BP = [0.0, 0.4, 0.8, 1.2, 1.6, 2.0, 2.4]
|
||||||
|
SMOOTH_DECEL_V = {
|
||||||
|
ECO: [0.00, -0.02, -0.05, -0.10, -0.25, -0.40, -0.60],
|
||||||
|
#ECO: [0.00, -0.08, -0.20, -0.30, -0.40, -0.70, -0.80],
|
||||||
|
NORMAL: [0.00, -0.13, -0.30, -0.55, -0.84, -1.12, -1.40],
|
||||||
|
SPORT: [0.00, -0.17, -0.40, -0.72, -1.05, -1.35, -1.65],
|
||||||
|
}
|
||||||
|
BRAKE_DEEPENING_JERK = {ECO: 0.5, NORMAL: 0.8, SPORT: 1.0}
|
||||||
|
BRAKE_RELEASE_JERK = 2.0
|
||||||
|
ACCEL_RISE_JERK = {ECO: 0.7, NORMAL: 1.2, SPORT: 1.6}
|
||||||
|
|
||||||
|
SMOOTH_DECEL_LOOKAHEAD_T = 3.0
|
||||||
|
MIN_SMOOTH_BRAKE_NEED = 0.2
|
||||||
|
HARD_BRAKE_TARGET_ACCEL = -1.5
|
||||||
|
HARD_BRAKE_NEED = 2.6
|
||||||
|
|
||||||
|
# Stop-imminent stand-down. The shaper's gentle bite is softer than the plan, so on a STOP approach it
|
||||||
|
# coasts the car in -> halts too close / "stop-roll-stop" creep. When the plan predicts a near-stop
|
||||||
|
# within the lookahead, stand the shaper down (full stock decel) so it stops at the proper gap with no
|
||||||
|
# coast. Keyed on the PREDICTED speed reaching ~0 (covers lead AND light/sign stops), NOT raw ego speed
|
||||||
|
# -- so non-stop low-speed braking (slowing to a moving follow) keeps the gentle onset at every speed.
|
||||||
|
STOP_IMMINENT_VEGO = 1.0 # m/s plan-predicted speed below this within the lookahead == stop coming
|
||||||
|
STOP_IMMINENT_LOOKAHEAD_T = 3.0 # s
|
||||||
|
|
||||||
|
# --- Convex brake-onset shaper (param-gated; ECO/SPORT only, NORMAL = stock passthrough) ---
|
||||||
|
# The grabby bite is the raw MPC plan: stock deepening uses a CONSTANT jerk (integrates to a LINEAR
|
||||||
|
# accel ramp) and min(slewed,raw) lets the deep raw plan win, so the bite passes through untouched.
|
||||||
|
# Fix: jerk-limit the deepening with a DEPTH-PROPORTIONAL jerk
|
||||||
|
# jerk(a) = ONSET_JERK0 + ONSET_JERK_GAIN * abs(a_current), capped at ONSET_JERK_MAX
|
||||||
|
# At the bite (a~0) the jerk is ONSET_JERK0 (gentle); it grows with decel depth, so the decel magnitude
|
||||||
|
# follows da/dt = j0 + k*a => a(t) = (j0/k)*(exp(k*t) - 1) -- the exponential-growth reference. The
|
||||||
|
# output is never deeper than the plan (only ever softer-or-equal during the bite) and converges to it.
|
||||||
|
# No velocity-debt feedback: it carried stale state across closely-spaced stop-and-go brakes and
|
||||||
|
# over-braked the next onset (verified). NORMAL omitted -> shaper never runs.
|
||||||
|
ONSET_JERK0 = {ECO: 0.15, SPORT: 0.25} # m/s^3 initial gentle jerk at the bite (target band 0.15-0.25)
|
||||||
|
ONSET_JERK_GAIN = {ECO: 0.9, SPORT: 1.5} # 1/s depth-proportional growth rate k (lowered: gentler jerk-build = smoother decel, less "jerky")
|
||||||
|
|
||||||
|
# Bounded softening: the gentle bite lags the plan (brakes shallower) at the very start. To keep the
|
||||||
|
# softening modest (so it never feels like "no brakes"), an INSTANTANEOUS-gap catch-up adds jerk when
|
||||||
|
# realized lags the plan by more than ONSET_GAP_SOFT, hard-capped at ONSET_JERK_MAX. This uses the
|
||||||
|
# current accel gap only (no integrated state) so nothing carries across closely-spaced brakes. Steady
|
||||||
|
# softening then settles near ONSET_GAP_SOFT; the hard cap keeps the catch from ever being a grab.
|
||||||
|
ONSET_GAP_SOFT = {ECO: 0.30, SPORT: 0.25} # m/s^2 tolerated shallower-than-plan gap before catch-up
|
||||||
|
ONSET_GAP_GAIN = {ECO: 4.0, SPORT: 5.0} # 1/s extra jerk per m/s^2 of gap beyond ONSET_GAP_SOFT
|
||||||
|
ONSET_JERK_MAX = {ECO: 1.1, SPORT: 1.4} # m/s^3 hard ceiling on convex-path jerk (lowered: smoother catch-up)
|
||||||
|
# Fast hand-back: once the plan leaves the gentle zone (no longer armed) but a soft gap is still open,
|
||||||
|
# close it at this FIRM jerk so the output catches the plan BEFORE braking gets firm -> no late-brake lag
|
||||||
|
# into the [-1.5,-1.0] band. Jerk-limited (not a snap), and never deeper than the plan, so not a grab.
|
||||||
|
ONSET_HANDBACK_JERK = {ECO: 2.2, SPORT: 3.0} # m/s^3 gap-close rate (lowered: gentler hand-back = less jounce/jerk)
|
||||||
|
|
||||||
|
# Arm gates (conservative). Only shape genuinely gentle onsets; firm/deep onsets fall to the stock
|
||||||
|
# never-weaker slew (they SHOULD bite). Two independent safety layers against late braking: (1) the
|
||||||
|
# PREDICTIVE brake_need gate declines to start a gentle bite when a firmer brake is seen within 3s, so
|
||||||
|
# we don't soften ahead of one; (2) the fast hand-back (ONSET_HANDBACK_JERK) closes any open soft gap
|
||||||
|
# before the plan reaches firm braking. Together: 0 firm-band ([-1.5,-1.0]) lag on the verified windows.
|
||||||
|
SOFT_ONSET_MAX_BRAKE_NEED = 0.9 # do NOT soften if a firmer brake is predicted within 3s
|
||||||
|
SOFT_ONSET_MAX_INSTANT_ACCEL = -0.7 # m/s^2 stop softening (fast hand-back) once raw is this deep
|
||||||
|
# Sticky re-arm: once an onset goes firm (instantaneously too deep) it latches OFF; require this many
|
||||||
|
# consecutive released/flat frames before a NEW soft window may open, so lead/SnG jitter cannot re-arm
|
||||||
|
# the bite every few hundred ms (flicker guard). Controller runs at the model rate (DT_MDL = 0.05 s).
|
||||||
|
SOFT_ONSET_REARM_FRAMES = 10 # frames (~0.5 s at 20 Hz model rate) of release before re-arm
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
"""
|
||||||
|
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||||
|
|
||||||
|
This file is part of sunnypilot and is licensed under the MIT License.
|
||||||
|
See the LICENSE.md file in the root directory for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from openpilot.sunnypilot.selfdrive.controls.lib.accel_personality.accel_controller import AccelController
|
||||||
|
from openpilot.common.realtime import DT_MDL
|
||||||
|
from openpilot.sunnypilot.selfdrive.controls.lib.accel_personality.accel_controller import AccelController as _AC # noqa: F401
|
||||||
|
from openpilot.sunnypilot.selfdrive.controls.lib.accel_personality.constants import \
|
||||||
|
ECO, NORMAL, SPORT, PERSONALITY_MIN, PERSONALITY_MAX, A_CRUISE_MAX_BP, RISE_RATE, \
|
||||||
|
STOCK_A_CRUISE_MAX_V, STOCK_RISE_RATE, HARD_BRAKE_TARGET_ACCEL, AccelerationPersonality, \
|
||||||
|
BRAKE_DEEPENING_JERK, ONSET_JERK0, ONSET_GAP_SOFT, ONSET_HANDBACK_JERK
|
||||||
|
|
||||||
|
# The convex onset brakes shallower than the plan during the bite, but the instantaneous-gap catch-up
|
||||||
|
# bounds how far it can lag, and it converges to the plan. The integrated velocity deficit over an
|
||||||
|
# armed brake stays under this conservative cap (no permanent offset; added stopping distance bounded).
|
||||||
|
_ONSET_VDEBT_BOUND = {ECO: 0.40, SPORT: 0.35}
|
||||||
|
|
||||||
|
T_IDXS = [0.0, 0.2, 0.4, 0.6, 0.8, 1.0, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0, 4.0]
|
||||||
|
_EPS = 1e-6
|
||||||
|
|
||||||
|
|
||||||
|
class FakeParams:
|
||||||
|
def __init__(self, store=None):
|
||||||
|
self.store = dict(store or {})
|
||||||
|
|
||||||
|
def get_bool(self, key):
|
||||||
|
return bool(self.store.get(key, False))
|
||||||
|
|
||||||
|
def get(self, key, return_default=False):
|
||||||
|
return int(self.store.get(key, 1))
|
||||||
|
|
||||||
|
def put(self, key, val, block=False):
|
||||||
|
self.store[key] = val
|
||||||
|
|
||||||
|
|
||||||
|
def make_sm(v_ego=20.0):
|
||||||
|
return {'carState': SimpleNamespace(vEgo=v_ego)}
|
||||||
|
|
||||||
|
|
||||||
|
def make_controller(enabled=True, personality=NORMAL, crash_cnt=0):
|
||||||
|
store = {"AccelPersonalityEnabled": enabled, "AccelPersonality": int(personality)}
|
||||||
|
ctrl = AccelController(CP=SimpleNamespace(), mpc=SimpleNamespace(crash_cnt=crash_cnt), params=FakeParams(store))
|
||||||
|
ctrl.update(make_sm())
|
||||||
|
return ctrl
|
||||||
|
|
||||||
|
|
||||||
|
def flat_traj(value):
|
||||||
|
return [float(value)] * len(T_IDXS)
|
||||||
|
|
||||||
|
|
||||||
|
def test_enum_source_parity():
|
||||||
|
assert (ECO, NORMAL, SPORT) == (AccelerationPersonality.eco, AccelerationPersonality.normal, AccelerationPersonality.sport)
|
||||||
|
assert (PERSONALITY_MIN, PERSONALITY_MAX) == (0, 2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_disabled_forces_normal_and_stock_ceiling():
|
||||||
|
ctrl = make_controller(enabled=False, personality=SPORT)
|
||||||
|
assert ctrl.personality() == NORMAL
|
||||||
|
assert not ctrl.enabled()
|
||||||
|
for v in (0.0, 10.0, 25.0, 40.0):
|
||||||
|
assert ctrl.get_max_accel(v) == pytest.approx(np.interp(v, A_CRUISE_MAX_BP, STOCK_A_CRUISE_MAX_V))
|
||||||
|
assert ctrl.get_rise_rate() == STOCK_RISE_RATE
|
||||||
|
|
||||||
|
|
||||||
|
def test_disabled_passes_brake_through():
|
||||||
|
ctrl = make_controller(enabled=False)
|
||||||
|
for raw in (-1.5, -0.5, 0.0, 1.0):
|
||||||
|
out = ctrl.smooth_target_accel(raw, flat_traj(raw), T_IDXS, should_stop=False)
|
||||||
|
assert out == pytest.approx(raw, abs=_EPS)
|
||||||
|
|
||||||
|
|
||||||
|
def test_normal_matches_stock():
|
||||||
|
ctrl = make_controller(personality=NORMAL)
|
||||||
|
for v in (0.0, 5.0, 10.0, 25.0, 40.0):
|
||||||
|
assert ctrl.get_max_accel(v) == pytest.approx(np.interp(v, A_CRUISE_MAX_BP, STOCK_A_CRUISE_MAX_V))
|
||||||
|
assert ctrl.get_rise_rate() == STOCK_RISE_RATE
|
||||||
|
|
||||||
|
|
||||||
|
def test_ceiling_ordering_eco_le_normal_lt_sport():
|
||||||
|
# ECO launch (v=0) intentionally matches stock for prompt take-off; ECO is strictly gentler only at
|
||||||
|
# cruise speeds. So ECO <= NORMAL everywhere, strictly below at cruise; SPORT strictly above NORMAL.
|
||||||
|
eco, normal, sport = (make_controller(personality=p) for p in (ECO, NORMAL, SPORT))
|
||||||
|
for v in (0.0, 10.0, 25.0, 40.0):
|
||||||
|
assert eco.get_max_accel(v) <= normal.get_max_accel(v) < sport.get_max_accel(v)
|
||||||
|
for v in (14.0, 25.0, 40.0):
|
||||||
|
assert eco.get_max_accel(v) < normal.get_max_accel(v)
|
||||||
|
|
||||||
|
|
||||||
|
def test_rise_rate_ordering():
|
||||||
|
# ECO rise == stock (prompt launch ramp) by design; SPORT firmer than both.
|
||||||
|
assert RISE_RATE[ECO] <= RISE_RATE[NORMAL] < RISE_RATE[SPORT]
|
||||||
|
|
||||||
|
|
||||||
|
def test_early_soft_braking_brakes_before_plan():
|
||||||
|
ctrl = make_controller(personality=NORMAL)
|
||||||
|
out = ctrl.smooth_target_accel(0.0, flat_traj(-1.0), T_IDXS, should_stop=False)
|
||||||
|
assert out < 0.0
|
||||||
|
assert ctrl.smooth_active()
|
||||||
|
assert ctrl.brake_need() == pytest.approx(1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stop_imminent_stands_down_but_moving_follow_shapes():
|
||||||
|
# Stop coming (plan speed -> ~0): stand down to stock decel so the gentle bite can't coast into the
|
||||||
|
# stop (creep). Slowing to a MOVING follow (plan stays > STOP_IMMINENT_VEGO): gentle onset stays active
|
||||||
|
# at every speed -> the gentle-brake goal is not regressed.
|
||||||
|
ctrl = make_controller(personality=ECO)
|
||||||
|
stopping = [3.0, 2.0, 1.0, 0.4, 0.0] + [0.0] * (len(T_IDXS) - 5)
|
||||||
|
out = ctrl.smooth_target_accel(-0.1, flat_traj(-1.0), T_IDXS, should_stop=False, speed_trajectory=stopping)
|
||||||
|
assert not ctrl.smooth_active()
|
||||||
|
assert out == pytest.approx(-0.1, abs=_EPS) # stock passthrough into the stop, no softening
|
||||||
|
moving = [8.0] * len(T_IDXS) # slowing to a moving follow, not a stop
|
||||||
|
ctrl.smooth_target_accel(-0.1, flat_traj(-1.0), T_IDXS, should_stop=False, speed_trajectory=moving)
|
||||||
|
assert ctrl.smooth_active() # gentle onset preserved (not stop-imminent)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("personality", [ECO, NORMAL, SPORT])
|
||||||
|
def test_never_weaker_than_plan_sustained_closing(personality):
|
||||||
|
# NORMAL/off: strict never-weaker (route 000003da regression guard). ECO/SPORT: the convex onset may
|
||||||
|
# lag the plan during the bite, but the INTEGRATED velocity deficit vs the plan stays bounded. This
|
||||||
|
# sequence also crosses the -1.5 emergency bypass (bit-exact raw thereafter).
|
||||||
|
ctrl = make_controller(personality=personality)
|
||||||
|
vdebt = 0.0
|
||||||
|
for raw in [0.0, -0.2, -0.5, -0.9, -1.2, -1.5] + [-1.5] * 40:
|
||||||
|
out = ctrl.smooth_target_accel(raw, flat_traj(raw), T_IDXS, should_stop=False)
|
||||||
|
if personality == NORMAL:
|
||||||
|
assert out <= raw + _EPS
|
||||||
|
else:
|
||||||
|
vdebt = max(0.0, vdebt + (out - raw) * DT_MDL)
|
||||||
|
assert vdebt <= _ONSET_VDEBT_BOUND[personality]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("personality", [ECO, NORMAL, SPORT])
|
||||||
|
def test_never_weaker_random_walk(personality):
|
||||||
|
# NORMAL strict never-weaker; ECO/SPORT integrated velocity deficit stays bounded even under an
|
||||||
|
# unrate-limited random plan (the deficit can never run away).
|
||||||
|
rng = np.random.default_rng(0)
|
||||||
|
ctrl = make_controller(personality=personality)
|
||||||
|
vdebt = 0.0
|
||||||
|
for _ in range(500):
|
||||||
|
raw = float(rng.uniform(-1.9, 1.5))
|
||||||
|
traj = flat_traj(raw - float(rng.uniform(0.0, 0.6)))
|
||||||
|
out = ctrl.smooth_target_accel(raw, traj, T_IDXS, should_stop=False)
|
||||||
|
if personality == NORMAL:
|
||||||
|
if raw < 0.0:
|
||||||
|
assert out <= raw + _EPS
|
||||||
|
else:
|
||||||
|
vdebt = max(0.0, vdebt + (out - raw) * DT_MDL)
|
||||||
|
assert vdebt <= _ONSET_VDEBT_BOUND[personality]
|
||||||
|
|
||||||
|
|
||||||
|
def test_normal_brake_bit_exact_vs_legacy_slew():
|
||||||
|
# NORMAL must be byte-identical to the legacy constant-jerk slew + min(slewed,raw) on a deepening ramp.
|
||||||
|
ctrl = make_controller(personality=NORMAL)
|
||||||
|
jmax = BRAKE_DEEPENING_JERK[NORMAL]
|
||||||
|
last = 0.0
|
||||||
|
for raw in [0.0, -0.1, -0.3, -0.45, -0.6, -0.6, -0.6, -0.5, -0.3, 0.0, 0.5]:
|
||||||
|
out = ctrl.smooth_target_accel(raw, flat_traj(raw), T_IDXS, should_stop=False)
|
||||||
|
if raw <= last: # deepening: legacy step-limited then min(.,raw)
|
||||||
|
legacy = min(max(raw, last - jmax * DT_MDL), raw) if raw < 0.0 else max(raw, last - jmax * DT_MDL)
|
||||||
|
assert out == pytest.approx(legacy, abs=_EPS)
|
||||||
|
last = out
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("personality", [ECO, SPORT])
|
||||||
|
def test_convex_onset_gentle_bite(personality):
|
||||||
|
# At a real brake onset the plan deepens gradually, so the gap stays within ONSET_GAP_SOFT and the
|
||||||
|
# first deepening tick must use only the gentle initial jerk ONSET_JERK0 (the soft bite), NOT bite hard.
|
||||||
|
ctrl = make_controller(personality=personality)
|
||||||
|
ctrl.smooth_target_accel(0.0, flat_traj(0.0), T_IDXS, should_stop=False) # seed last=0
|
||||||
|
raw = -0.5 * ONSET_GAP_SOFT[personality] # within the gap budget -> gentle bite, no catch-up
|
||||||
|
out = ctrl.smooth_target_accel(raw, flat_traj(raw), T_IDXS, should_stop=False)
|
||||||
|
j_init = abs(out - 0.0) / DT_MDL # realized first-tick jerk == ONSET_JERK0 (gentle bite)
|
||||||
|
assert j_init == pytest.approx(ONSET_JERK0[personality], abs=1e-6)
|
||||||
|
assert out > raw # softened: shallower than the plan, NOT passed through
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("personality", [ECO, SPORT])
|
||||||
|
def test_convex_onset_velocity_deficit_bounded_and_converges(personality):
|
||||||
|
# Integrated velocity deficit vs plan over a sustained moderate brake stays bounded, and the controller
|
||||||
|
# CONVERGES to the plan (no permanent velocity offset -> added stopping distance is a bounded transient).
|
||||||
|
ctrl = make_controller(personality=personality)
|
||||||
|
vdebt = 0.0
|
||||||
|
last = 0.0
|
||||||
|
raw = -0.9 # armed (raw > SOFT_ONSET_MAX_INSTANT_ACCEL = -1.0)
|
||||||
|
for _ in range(500):
|
||||||
|
out = ctrl.smooth_target_accel(raw, flat_traj(raw), T_IDXS, should_stop=False)
|
||||||
|
vdebt = max(0.0, vdebt + (out - raw) * DT_MDL) # accrue only shallower-than-plan deficit
|
||||||
|
last = out
|
||||||
|
assert vdebt <= _ONSET_VDEBT_BOUND[personality]
|
||||||
|
assert last == pytest.approx(raw, abs=1e-3) # converged to the plan, no permanent offset
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("personality", [ECO, SPORT])
|
||||||
|
def test_convex_onset_no_jerk_snap(personality):
|
||||||
|
# A gentle armed bite opens a soft gap; when the plan then deepens past the gentle zone the gap is
|
||||||
|
# closed by the FAST hand-back -- still jerk-limited, never a 1-frame snap to the plan. Max realized
|
||||||
|
# jerk anywhere on the convex path must not exceed the hand-back ceiling.
|
||||||
|
ctrl = make_controller(personality=personality)
|
||||||
|
ctrl.smooth_target_accel(0.0, flat_traj(0.0), T_IDXS, should_stop=False)
|
||||||
|
prev = 0.0
|
||||||
|
worst = 0.0
|
||||||
|
for raw in [-0.3] * 10 + [-0.9] * 40: # gentle onset, then deepen past the gentle zone
|
||||||
|
out = ctrl.smooth_target_accel(raw, flat_traj(raw), T_IDXS, should_stop=False)
|
||||||
|
worst = max(worst, abs(out - prev) / DT_MDL)
|
||||||
|
prev = out
|
||||||
|
assert worst <= ONSET_HANDBACK_JERK[personality] + _EPS
|
||||||
|
|
||||||
|
|
||||||
|
def test_hard_brake_bypass():
|
||||||
|
ctrl = make_controller(personality=ECO)
|
||||||
|
raw = HARD_BRAKE_TARGET_ACCEL - 0.5
|
||||||
|
out = ctrl.smooth_target_accel(raw, flat_traj(raw), T_IDXS, should_stop=False)
|
||||||
|
assert out == pytest.approx(raw, abs=_EPS)
|
||||||
|
assert ctrl.bypassed()
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_stop_bypass():
|
||||||
|
ctrl = make_controller(personality=ECO)
|
||||||
|
out = ctrl.smooth_target_accel(-1.0, flat_traj(-1.0), T_IDXS, should_stop=True)
|
||||||
|
assert out == pytest.approx(-1.0, abs=_EPS)
|
||||||
|
assert ctrl.bypassed()
|
||||||
|
|
||||||
|
|
||||||
|
def test_fcw_crash_cnt_bypass():
|
||||||
|
ctrl = make_controller(personality=ECO, crash_cnt=3)
|
||||||
|
out = ctrl.smooth_target_accel(-1.0, flat_traj(-1.0), T_IDXS, should_stop=False)
|
||||||
|
assert out == pytest.approx(-1.0, abs=_EPS)
|
||||||
|
assert ctrl.bypassed()
|
||||||
|
|
||||||
|
|
||||||
|
def test_e2e_brake_passthrough():
|
||||||
|
ctrl = make_controller(personality=ECO)
|
||||||
|
out = ctrl.smooth_target_accel(-1.0, flat_traj(-1.0), T_IDXS, should_stop=False, stock_brake=True)
|
||||||
|
assert out == pytest.approx(-1.0, abs=_EPS)
|
||||||
|
assert not ctrl.smooth_active()
|
||||||
|
|
||||||
|
|
||||||
|
def test_out_of_range_personality_clamps():
|
||||||
|
ctrl = AccelController(CP=SimpleNamespace(), mpc=SimpleNamespace(crash_cnt=0),
|
||||||
|
params=FakeParams({"AccelPersonalityEnabled": True, "AccelPersonality": 99}))
|
||||||
|
ctrl.update(make_sm())
|
||||||
|
assert ctrl.personality() == PERSONALITY_MAX
|
||||||
|
|
||||||
|
|
||||||
|
def test_reset_passes_through():
|
||||||
|
ctrl = make_controller(personality=ECO)
|
||||||
|
out = ctrl.smooth_target_accel(0.0, flat_traj(-1.0), T_IDXS, should_stop=False, reset=True)
|
||||||
|
assert out == pytest.approx(0.0, abs=_EPS)
|
||||||
|
assert not ctrl.bypassed()
|
||||||
@@ -1,17 +1,46 @@
|
|||||||
|
from openpilot.common.realtime import DT_MDL
|
||||||
|
|
||||||
|
|
||||||
class WMACConstants:
|
class WMACConstants:
|
||||||
# Lead detection parameters
|
TRAJECTORY_SIZE = 33
|
||||||
LEAD_WINDOW_SIZE = 6 # Stable detection window
|
PARAM_READ_FRAMES = max(1, int(round(1.0 / DT_MDL)))
|
||||||
LEAD_PROB = 0.45 # Balanced threshold for lead detection
|
|
||||||
|
|
||||||
# Slow down detection parameters
|
EMERGENCY_HOLD_FRAMES = max(1, int(round(0.75 / DT_MDL)))
|
||||||
SLOW_DOWN_WINDOW_SIZE = 5 # Responsive but stable
|
MIN_MODE_DURATION = {'acc': max(1, int(round(0.6 / DT_MDL))), 'blended': max(1, int(round(0.5 / DT_MDL)))}
|
||||||
SLOW_DOWN_PROB = 0.3 # Balanced threshold for slow down scenarios
|
ENTER_BLENDED_FRAMES = max(1, int(round(0.4 / DT_MDL)))
|
||||||
|
EXIT_BLENDED_FRAMES = max(1, int(round(0.35 / DT_MDL)))
|
||||||
|
STANDSTILL_FRAMES = max(1, int(round(0.2 / DT_MDL)))
|
||||||
|
|
||||||
# Optimized slow down distance curve - smooth and progressive
|
LEAD_PROB = 0.45
|
||||||
|
LEAD_EXIT_PROB = 0.25
|
||||||
|
LEAD_RISE_RATE = 1.0
|
||||||
|
LEAD_FALL_RATE = 0.35
|
||||||
|
RADAR_LEAD_ACC_PROB = 0.5
|
||||||
|
RADAR_LEAD_ACC_EXIT_PROB = 0.4
|
||||||
|
RADAR_LEAD_ACC_RISE_RATE = 1.0
|
||||||
|
RADAR_LEAD_ACC_FALL_RATE = 0.25
|
||||||
|
RADAR_LEAD_ACC_MAX_DREL = 80.0
|
||||||
|
RADAR_LEAD_ACC_MAX_TTC = 6.0
|
||||||
|
RADAR_LEAD_ACC_MIN_CLOSING_SPEED = -0.5
|
||||||
|
|
||||||
|
SLOW_DOWN_PROB = 0.5
|
||||||
|
SLOW_DOWN_EXIT_PROB = 0.4
|
||||||
|
SLOW_DOWN_RISE_RATE = 0.65
|
||||||
|
SLOW_DOWN_FALL_RATE = 0.15
|
||||||
SLOW_DOWN_BP = [0., 10., 20., 30., 40., 50., 55., 60.]
|
SLOW_DOWN_BP = [0., 10., 20., 30., 40., 50., 55., 60.]
|
||||||
SLOW_DOWN_DIST = [32., 46., 64., 86., 108., 130., 145., 165.]
|
SLOW_DOWN_DIST = [32., 46., 64., 86., 108., 130., 145., 165.]
|
||||||
|
URGENT_SLOW_DOWN_PROB = 0.85
|
||||||
|
|
||||||
# Slowness detection parameters
|
MODEL_DECEL_START = -0.5
|
||||||
SLOWNESS_WINDOW_SIZE = 10 # Stable slowness detection
|
MODEL_DECEL_RANGE = 2.0
|
||||||
SLOWNESS_PROB = 0.55 # Clear threshold for slowness
|
ENDPOINT_URGENCY_GAIN = 1.3
|
||||||
SLOWNESS_CRUISE_OFFSET = 1.025 # Conservative cruise speed offset
|
CRITICAL_ENDPOINT_FACTOR = 0.3
|
||||||
|
CRITICAL_URGENCY_GAIN = 1.5
|
||||||
|
SPEED_URGENCY_MIN = 25.0
|
||||||
|
SPEED_URGENCY_RANGE = 80.0
|
||||||
|
|
||||||
|
SLOWNESS_PROB = 0.55
|
||||||
|
SLOWNESS_EXIT_PROB = 0.45
|
||||||
|
SLOWNESS_RISE_RATE = 0.35
|
||||||
|
SLOWNESS_FALL_RATE = 0.5
|
||||||
|
SLOWNESS_CRUISE_OFFSET = 1.025
|
||||||
|
|||||||
@@ -6,129 +6,116 @@ See the LICENSE.md file in the root directory for more details.
|
|||||||
"""
|
"""
|
||||||
# Version = 2025-6-30
|
# Version = 2025-6-30
|
||||||
|
|
||||||
from cereal import messaging
|
|
||||||
from opendbc.car import structs
|
|
||||||
from numpy import interp
|
|
||||||
from openpilot.common.params import Params
|
|
||||||
from openpilot.common.realtime import DT_MDL
|
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.dec.constants import WMACConstants
|
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
# d-e2e, from modeldata.h
|
from cereal import messaging
|
||||||
TRAJECTORY_SIZE = 33
|
from numpy import interp
|
||||||
SET_MODE_TIMEOUT = 15
|
from opendbc.car import structs
|
||||||
|
from openpilot.common.params import Params
|
||||||
|
from openpilot.sunnypilot.selfdrive.controls.lib.dec.constants import WMACConstants
|
||||||
|
|
||||||
# Define the valid mode types
|
|
||||||
ModeType = Literal['acc', 'blended']
|
ModeType = Literal['acc', 'blended']
|
||||||
|
|
||||||
|
|
||||||
class SmoothKalmanFilter:
|
def clip01(value: float) -> float:
|
||||||
"""Enhanced Kalman filter with smoothing for stable decision making."""
|
return max(0.0, min(1.0, float(value)))
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
def add_data(self, measurement):
|
class SmoothedSignal:
|
||||||
if len(self.history) >= self.max_history:
|
def __init__(self, rise_rate: float, fall_rate: float, initial_value: float = 0.0):
|
||||||
self.history.pop(0)
|
self.rise_rate = clip01(rise_rate)
|
||||||
self.history.append(measurement)
|
self.fall_rate = clip01(fall_rate)
|
||||||
|
self.value = clip01(initial_value)
|
||||||
|
|
||||||
if not self.initialized:
|
def update(self, measurement: float) -> float:
|
||||||
self.x = measurement
|
measurement = clip01(measurement)
|
||||||
self.initialized = True
|
rate = self.rise_rate if measurement > self.value else self.fall_rate
|
||||||
self.confidence = 0.1
|
self.value += (measurement - self.value) * rate
|
||||||
return
|
return self.value
|
||||||
|
|
||||||
self.P = self.alpha * self.P + self.Q
|
def reset(self, value: float = 0.0) -> None:
|
||||||
|
self.value = clip01(value)
|
||||||
|
|
||||||
K = self.P / (self.P + self.R)
|
|
||||||
effective_K = K * (1.0 - self.smoothing_factor) + self.smoothing_factor * 0.1
|
|
||||||
|
|
||||||
innovation = measurement - self.x
|
class HysteresisSignal:
|
||||||
self.x = self.x + effective_K * innovation
|
def __init__(self, enter_threshold: float, exit_threshold: float, rise_rate: float, fall_rate: float):
|
||||||
self.P = (1 - effective_K) * self.P
|
self.enter_threshold = clip01(enter_threshold)
|
||||||
|
self.exit_threshold = clip01(exit_threshold)
|
||||||
|
self.filter = SmoothedSignal(rise_rate, fall_rate)
|
||||||
|
self.active = False
|
||||||
|
|
||||||
if abs(innovation) < 0.1:
|
def update(self, measurement: float) -> bool:
|
||||||
self.confidence = min(1.0, self.confidence + 0.05)
|
value = self.filter.update(measurement)
|
||||||
else:
|
threshold = self.exit_threshold if self.active else self.enter_threshold
|
||||||
self.confidence = max(0.1, self.confidence - 0.02)
|
self.active = value > threshold
|
||||||
|
return self.active
|
||||||
|
|
||||||
def get_value(self):
|
def reset(self) -> None:
|
||||||
return self.x if self.initialized else None
|
self.filter.reset()
|
||||||
|
self.active = False
|
||||||
|
|
||||||
def get_confidence(self):
|
@property
|
||||||
return self.confidence
|
def value(self) -> float:
|
||||||
|
return self.filter.value
|
||||||
def reset_data(self):
|
|
||||||
self.initialized = False
|
|
||||||
self.history = []
|
|
||||||
self.confidence = 0.0
|
|
||||||
|
|
||||||
|
|
||||||
class ModeTransitionManager:
|
class ModeTransitionManager:
|
||||||
"""Manages smooth transitions between driving modes with hysteresis."""
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.current_mode: ModeType = 'acc'
|
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.mode_duration = 0
|
||||||
self.emergency_override = False
|
self._pending_mode: ModeType = 'acc'
|
||||||
|
self._pending_count = 0
|
||||||
|
self._blended_hold_frames = 0
|
||||||
|
|
||||||
def request_mode(self, mode: ModeType, confidence: float = 1.0, emergency: bool = False):
|
def request_mode(self, mode: ModeType, immediate: bool = False, hold_frames: int = 0, cancel_hold: bool = False) -> None:
|
||||||
# Emergency override for critical situations (stops, collisions)
|
if immediate:
|
||||||
if emergency:
|
self._blended_hold_frames = max(self._blended_hold_frames, hold_frames)
|
||||||
self.emergency_override = True
|
self._pending_mode = mode
|
||||||
self.current_mode = mode
|
self._pending_count = 0
|
||||||
self.transition_timeout = SET_MODE_TIMEOUT
|
self._switch_mode(mode)
|
||||||
self.mode_duration = 0
|
|
||||||
return
|
return
|
||||||
|
|
||||||
self.mode_confidence[mode] = min(1.0, self.mode_confidence[mode] + 0.1 * confidence)
|
if cancel_hold and mode == 'acc':
|
||||||
for m in self.mode_confidence:
|
self._blended_hold_frames = 0
|
||||||
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._blended_hold_frames > 0:
|
||||||
if self.mode_duration < self.min_mode_duration and not self.emergency_override:
|
mode = 'blended'
|
||||||
|
|
||||||
|
if mode == self.current_mode:
|
||||||
|
self._pending_mode = mode
|
||||||
|
self._pending_count = 0
|
||||||
return
|
return
|
||||||
|
|
||||||
# Hysteresis: higher threshold for mode changes
|
if mode != self._pending_mode:
|
||||||
confidence_threshold = 0.6 if mode != self.current_mode else 0.3 # Lower threshold for faster response
|
self._pending_mode = mode
|
||||||
|
self._pending_count = 1
|
||||||
|
else:
|
||||||
|
self._pending_count += 1
|
||||||
|
|
||||||
if self.mode_confidence[mode] > confidence_threshold:
|
if self.mode_duration < WMACConstants.MIN_MODE_DURATION[self.current_mode]:
|
||||||
if mode != self.current_mode and self.transition_timeout == 0:
|
return
|
||||||
self.transition_timeout = SET_MODE_TIMEOUT
|
|
||||||
self.current_mode = mode
|
|
||||||
self.mode_duration = 0
|
|
||||||
|
|
||||||
def update(self):
|
required_count = WMACConstants.ENTER_BLENDED_FRAMES if mode == 'blended' else WMACConstants.EXIT_BLENDED_FRAMES
|
||||||
if self.transition_timeout > 0:
|
if self._pending_count >= required_count:
|
||||||
self.transition_timeout -= 1
|
self._switch_mode(mode)
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
if self._blended_hold_frames > 0:
|
||||||
|
self._blended_hold_frames -= 1
|
||||||
self.mode_duration += 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:
|
def get_mode(self) -> ModeType:
|
||||||
return self.current_mode
|
return self.current_mode
|
||||||
|
|
||||||
|
def _switch_mode(self, mode: ModeType) -> None:
|
||||||
|
if mode == self.current_mode:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.current_mode = mode
|
||||||
|
self.mode_duration = 0
|
||||||
|
self._pending_mode = mode
|
||||||
|
self._pending_count = 0
|
||||||
|
|
||||||
|
|
||||||
class DynamicExperimentalController:
|
class DynamicExperimentalController:
|
||||||
def __init__(self, CP: structs.CarParams, mpc, params=None):
|
def __init__(self, CP: structs.CarParams, mpc, params=None):
|
||||||
@@ -142,35 +129,33 @@ class DynamicExperimentalController:
|
|||||||
|
|
||||||
self._mode_manager = ModeTransitionManager()
|
self._mode_manager = ModeTransitionManager()
|
||||||
|
|
||||||
# Smooth filters for stable decision making with faster response for critical scenarios
|
self._lead_tracker = HysteresisSignal(
|
||||||
self._lead_filter = SmoothKalmanFilter(
|
enter_threshold=WMACConstants.LEAD_PROB,
|
||||||
measurement_noise=0.15,
|
exit_threshold=WMACConstants.LEAD_EXIT_PROB,
|
||||||
process_noise=0.05,
|
rise_rate=WMACConstants.LEAD_RISE_RATE,
|
||||||
alpha=1.02,
|
fall_rate=WMACConstants.LEAD_FALL_RATE,
|
||||||
smoothing_factor=0.8
|
)
|
||||||
|
self._radar_acc_lead_tracker = HysteresisSignal(
|
||||||
|
enter_threshold=WMACConstants.RADAR_LEAD_ACC_PROB,
|
||||||
|
exit_threshold=WMACConstants.RADAR_LEAD_ACC_EXIT_PROB,
|
||||||
|
rise_rate=WMACConstants.RADAR_LEAD_ACC_RISE_RATE,
|
||||||
|
fall_rate=WMACConstants.RADAR_LEAD_ACC_FALL_RATE,
|
||||||
|
)
|
||||||
|
self._slow_down_tracker = HysteresisSignal(
|
||||||
|
enter_threshold=WMACConstants.SLOW_DOWN_PROB,
|
||||||
|
exit_threshold=WMACConstants.SLOW_DOWN_EXIT_PROB,
|
||||||
|
rise_rate=WMACConstants.SLOW_DOWN_RISE_RATE,
|
||||||
|
fall_rate=WMACConstants.SLOW_DOWN_FALL_RATE,
|
||||||
|
)
|
||||||
|
self._slowness_tracker = HysteresisSignal(
|
||||||
|
enter_threshold=WMACConstants.SLOWNESS_PROB,
|
||||||
|
exit_threshold=WMACConstants.SLOWNESS_EXIT_PROB,
|
||||||
|
rise_rate=WMACConstants.SLOWNESS_RISE_RATE,
|
||||||
|
fall_rate=WMACConstants.SLOWNESS_FALL_RATE,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._slow_down_filter = SmoothKalmanFilter(
|
|
||||||
measurement_noise=0.1,
|
|
||||||
process_noise=0.1,
|
|
||||||
alpha=1.05,
|
|
||||||
smoothing_factor=0.7
|
|
||||||
)
|
|
||||||
|
|
||||||
self._slowness_filter = SmoothKalmanFilter(
|
|
||||||
measurement_noise=0.1,
|
|
||||||
process_noise=0.06,
|
|
||||||
alpha=1.015,
|
|
||||||
smoothing_factor=0.92
|
|
||||||
)
|
|
||||||
|
|
||||||
self._mpc_fcw_filter = SmoothKalmanFilter(
|
|
||||||
measurement_noise=0.2,
|
|
||||||
process_noise=0.1,
|
|
||||||
alpha=1.1,
|
|
||||||
smoothing_factor=0.5
|
|
||||||
)
|
|
||||||
self._has_lead_filtered = False
|
self._has_lead_filtered = False
|
||||||
|
self._has_radar_acc_lead = False
|
||||||
self._has_slow_down = False
|
self._has_slow_down = False
|
||||||
self._has_slowness = False
|
self._has_slowness = False
|
||||||
self._has_mpc_fcw = False
|
self._has_mpc_fcw = False
|
||||||
@@ -179,13 +164,14 @@ class DynamicExperimentalController:
|
|||||||
self._has_standstill = False
|
self._has_standstill = False
|
||||||
self._mpc_fcw_crash_cnt = 0
|
self._mpc_fcw_crash_cnt = 0
|
||||||
self._standstill_count = 0
|
self._standstill_count = 0
|
||||||
# debug
|
|
||||||
self._endpoint_x = float('inf')
|
self._endpoint_x = float('inf')
|
||||||
self._expected_distance = 0.0
|
self._expected_distance = 0.0
|
||||||
self._trajectory_valid = False
|
self._trajectory_valid = False
|
||||||
|
self._raw_urgency = 0.0
|
||||||
|
|
||||||
def _read_params(self) -> None:
|
def _read_params(self) -> None:
|
||||||
if self._frame % int(1. / DT_MDL) == 0:
|
if self._frame % WMACConstants.PARAM_READ_FRAMES == 0:
|
||||||
self._enabled = self._params.get_bool("DynamicExperimentalControl")
|
self._enabled = self._params.get_bool("DynamicExperimentalControl")
|
||||||
|
|
||||||
def mode(self) -> str:
|
def mode(self) -> str:
|
||||||
@@ -198,7 +184,6 @@ class DynamicExperimentalController:
|
|||||||
return self._active
|
return self._active
|
||||||
|
|
||||||
def set_mpc_fcw_crash_cnt(self) -> None:
|
def set_mpc_fcw_crash_cnt(self) -> None:
|
||||||
"""Set MPC FCW crash count"""
|
|
||||||
self._mpc_fcw_crash_cnt = self._mpc.crash_cnt
|
self._mpc_fcw_crash_cnt = self._mpc.crash_cnt
|
||||||
|
|
||||||
def _update_calculations(self, sm: messaging.SubMaster) -> None:
|
def _update_calculations(self, sm: messaging.SubMaster) -> None:
|
||||||
@@ -210,179 +195,109 @@ class DynamicExperimentalController:
|
|||||||
self._v_cruise_kph = car_state.vCruise
|
self._v_cruise_kph = car_state.vCruise
|
||||||
self._has_standstill = car_state.standstill
|
self._has_standstill = car_state.standstill
|
||||||
|
|
||||||
# standstill detection
|
|
||||||
if self._has_standstill:
|
if self._has_standstill:
|
||||||
self._standstill_count = min(20, self._standstill_count + 1)
|
self._standstill_count = min(WMACConstants.STANDSTILL_FRAMES * 3, self._standstill_count + 1)
|
||||||
else:
|
else:
|
||||||
self._standstill_count = max(0, self._standstill_count - 1)
|
self._standstill_count = max(0, self._standstill_count - 1)
|
||||||
|
|
||||||
# Lead detection
|
self._has_lead_filtered = self._lead_tracker.update(float(lead_one.status))
|
||||||
self._lead_filter.add_data(float(lead_one.status))
|
self._has_radar_acc_lead = self._radar_acc_lead_tracker.update(self._radar_acc_lead_score(lead_one))
|
||||||
lead_value = self._lead_filter.get_value() or 0.0
|
self._has_mpc_fcw = self._mpc_fcw_crash_cnt > 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)
|
self._calculate_slow_down(md)
|
||||||
|
|
||||||
# Slowness detection
|
if self._standstill_count > WMACConstants.STANDSTILL_FRAMES or self._has_slow_down:
|
||||||
if not (self._standstill_count > 5) and not self._has_slow_down:
|
self._slowness_tracker.reset()
|
||||||
|
self._has_slowness = False
|
||||||
|
else:
|
||||||
current_slowness = float(self._v_ego_kph <= (self._v_cruise_kph * WMACConstants.SLOWNESS_CRUISE_OFFSET))
|
current_slowness = float(self._v_ego_kph <= (self._v_cruise_kph * WMACConstants.SLOWNESS_CRUISE_OFFSET))
|
||||||
self._slowness_filter.add_data(current_slowness)
|
self._has_slowness = self._slowness_tracker.update(current_slowness)
|
||||||
slowness_value = self._slowness_filter.get_value() or 0.0
|
|
||||||
|
|
||||||
# Hysteresis for slowness
|
def _calculate_slow_down(self, md) -> None:
|
||||||
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._endpoint_x = float('inf')
|
||||||
|
self._expected_distance = 0.0
|
||||||
self._trajectory_valid = False
|
self._trajectory_valid = False
|
||||||
|
|
||||||
#Require exact trajectory size
|
urgency = self._model_action_urgency(md)
|
||||||
position_valid = len(md.position.x) == TRAJECTORY_SIZE
|
position_valid = len(md.position.x) == WMACConstants.TRAJECTORY_SIZE
|
||||||
orientation_valid = len(md.orientation.x) == TRAJECTORY_SIZE
|
|
||||||
|
|
||||||
if not (position_valid and orientation_valid):
|
if position_valid:
|
||||||
# Invalid trajectory - this itself might indicate a stop scenario
|
self._trajectory_valid = True
|
||||||
# Apply moderate urgency for incomplete trajectories at speed
|
self._endpoint_x = md.position.x[WMACConstants.TRAJECTORY_SIZE - 1]
|
||||||
if self._v_ego_kph > 20.0:
|
self._expected_distance = interp(self._v_ego_kph, WMACConstants.SLOW_DOWN_BP, WMACConstants.SLOW_DOWN_DIST)
|
||||||
urgency = 0.3
|
urgency = max(urgency, self._endpoint_urgency(self._endpoint_x, self._expected_distance))
|
||||||
|
|
||||||
self._slow_down_filter.add_data(urgency)
|
self._raw_urgency = clip01(urgency)
|
||||||
urgency_filtered = self._slow_down_filter.get_value() or 0.0
|
self._has_slow_down = self._slow_down_tracker.update(self._raw_urgency)
|
||||||
self._has_slow_down = urgency_filtered > WMACConstants.SLOW_DOWN_PROB
|
self._urgency = self._slow_down_tracker.value
|
||||||
self._urgency = urgency_filtered
|
|
||||||
return
|
|
||||||
|
|
||||||
# We have a valid full trajectory
|
def _radar_acc_lead_score(self, lead_one) -> float:
|
||||||
self._trajectory_valid = True
|
if not lead_one.status:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
# Use the exact endpoint (33rd point, index 32)
|
d_rel = float(getattr(lead_one, 'dRel', float('inf')))
|
||||||
endpoint_x = md.position.x[TRAJECTORY_SIZE - 1]
|
v_rel = float(getattr(lead_one, 'vRel', 0.0))
|
||||||
self._endpoint_x = endpoint_x
|
if d_rel <= WMACConstants.RADAR_LEAD_ACC_MAX_DREL:
|
||||||
|
return 1.0
|
||||||
|
if v_rel <= WMACConstants.RADAR_LEAD_ACC_MIN_CLOSING_SPEED and d_rel / max(-v_rel, 0.1) <= WMACConstants.RADAR_LEAD_ACC_MAX_TTC:
|
||||||
|
return 1.0
|
||||||
|
return 0.0
|
||||||
|
|
||||||
# Get expected distance based on current speed using tuned constants
|
def _model_action_urgency(self, md) -> float:
|
||||||
expected_distance = interp(self._v_ego_kph,
|
action = getattr(md, 'action', None)
|
||||||
WMACConstants.SLOW_DOWN_BP,
|
if action is None:
|
||||||
WMACConstants.SLOW_DOWN_DIST)
|
return 0.0
|
||||||
self._expected_distance = expected_distance
|
|
||||||
|
|
||||||
# Calculate urgency based on trajectory shortage
|
urgency = 1.0 if getattr(action, 'shouldStop', False) else 0.0
|
||||||
if endpoint_x < expected_distance:
|
desired_accel = getattr(action, 'desiredAcceleration', 0.0)
|
||||||
shortage = expected_distance - endpoint_x
|
if desired_accel < WMACConstants.MODEL_DECEL_START:
|
||||||
shortage_ratio = shortage / expected_distance
|
urgency = max(urgency, min(1.0, (WMACConstants.MODEL_DECEL_START - desired_accel) / WMACConstants.MODEL_DECEL_RANGE))
|
||||||
|
return urgency
|
||||||
|
|
||||||
# Base urgency on shortage ratio
|
def _endpoint_urgency(self, endpoint_x: float, expected_distance: float) -> float:
|
||||||
urgency = min(1.0, shortage_ratio * 2.0)
|
if endpoint_x >= expected_distance:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
# Increase urgency for very short trajectories (imminent stops)
|
shortage_ratio = (expected_distance - endpoint_x) / expected_distance
|
||||||
critical_distance = expected_distance * 0.3
|
urgency = min(1.0, shortage_ratio * WMACConstants.ENDPOINT_URGENCY_GAIN)
|
||||||
if endpoint_x < critical_distance:
|
|
||||||
urgency = min(1.0, urgency * 2.0)
|
|
||||||
|
|
||||||
# Speed-based urgency adjustment
|
if endpoint_x < expected_distance * WMACConstants.CRITICAL_ENDPOINT_FACTOR:
|
||||||
if self._v_ego_kph > 25.0:
|
urgency = min(1.0, urgency * WMACConstants.CRITICAL_URGENCY_GAIN)
|
||||||
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
|
if self._v_ego_kph > WMACConstants.SPEED_URGENCY_MIN:
|
||||||
self._slow_down_filter.add_data(urgency)
|
speed_factor = 1.0 + (self._v_ego_kph - WMACConstants.SPEED_URGENCY_MIN) / WMACConstants.SPEED_URGENCY_RANGE
|
||||||
urgency_filtered = self._slow_down_filter.get_value() or 0.0
|
urgency = min(1.0, urgency * speed_factor)
|
||||||
|
|
||||||
# Update state with lower threshold for better stop detection
|
return urgency
|
||||||
self._has_slow_down = urgency_filtered > (WMACConstants.SLOW_DOWN_PROB * 0.8)
|
|
||||||
self._urgency = urgency_filtered
|
|
||||||
|
|
||||||
def _radarless_mode(self) -> None:
|
def _desired_mode(self) -> tuple[ModeType, bool]:
|
||||||
"""Radarless mode decision logic with emergency handling."""
|
if not self._CP.radarUnavailable and self._has_radar_acc_lead:
|
||||||
|
return 'acc', False
|
||||||
|
|
||||||
# EMERGENCY: MPC FCW - immediate blended mode
|
|
||||||
if self._has_mpc_fcw:
|
if self._has_mpc_fcw:
|
||||||
self._mode_manager.request_mode('blended', confidence=1.0, emergency=True)
|
return 'blended', True
|
||||||
return
|
|
||||||
|
|
||||||
# Standstill: use blended
|
standstill = self._standstill_count > WMACConstants.STANDSTILL_FRAMES
|
||||||
if self._standstill_count > 3:
|
urgent_slow_down = self._has_slow_down and self._raw_urgency > WMACConstants.URGENT_SLOW_DOWN_PROB
|
||||||
self._mode_manager.request_mode('blended', confidence=0.9)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Slow down scenarios: emergency for high urgency, normal for lower urgency
|
if self._CP.radarUnavailable:
|
||||||
if self._has_slow_down:
|
if standstill or self._has_slow_down:
|
||||||
if self._urgency > 0.7:
|
return 'blended', urgent_slow_down
|
||||||
# Emergency: immediate blended mode for high urgency stops
|
return 'acc', False
|
||||||
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 standstill or self._has_slow_down:
|
||||||
if self._has_slowness and not self._has_slow_down:
|
return 'blended', urgent_slow_down
|
||||||
self._mode_manager.request_mode('acc', confidence=0.8)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Default: ACC
|
return 'acc', False
|
||||||
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()
|
||||||
|
|
||||||
self.set_mpc_fcw_crash_cnt()
|
self.set_mpc_fcw_crash_cnt()
|
||||||
|
|
||||||
self._update_calculations(sm)
|
self._update_calculations(sm)
|
||||||
|
|
||||||
if self._CP.radarUnavailable:
|
mode, immediate = self._desired_mode()
|
||||||
self._radarless_mode()
|
self._mode_manager.request_mode(mode, immediate=immediate, hold_frames=WMACConstants.EMERGENCY_HOLD_FRAMES,
|
||||||
else:
|
cancel_hold=self._has_radar_acc_lead)
|
||||||
self._radar_mode()
|
|
||||||
|
|
||||||
self._mode_manager.update()
|
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,94 +0,0 @@
|
|||||||
import pytest
|
|
||||||
|
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController
|
|
||||||
|
|
||||||
class MockLeadOne:
|
|
||||||
def __init__(self, status=0.0):
|
|
||||||
self.status = status
|
|
||||||
|
|
||||||
class MockRadarState:
|
|
||||||
def __init__(self, status=0.0):
|
|
||||||
self.leadOne = MockLeadOne(status=status)
|
|
||||||
|
|
||||||
class MockCarState:
|
|
||||||
def __init__(self, vEgo=0.0, vCruise=0.0, standstill=False):
|
|
||||||
self.vEgo = vEgo
|
|
||||||
self.vCruise = vCruise
|
|
||||||
self.standstill = standstill
|
|
||||||
|
|
||||||
class MockModelData:
|
|
||||||
def __init__(self, valid=True):
|
|
||||||
size = 33 if valid else 10 # incomplete if invalid
|
|
||||||
self.position = type("Pos", (), {"x": [0.0] * size})()
|
|
||||||
self.orientation = type("Ori", (), {"x": [0.0] * size})()
|
|
||||||
|
|
||||||
class MockSelfDriveState:
|
|
||||||
def __init__(self, experimentalMode=False):
|
|
||||||
self.experimentalMode = experimentalMode
|
|
||||||
|
|
||||||
class MockParams:
|
|
||||||
def get_bool(self, name):
|
|
||||||
return True
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def default_sm():
|
|
||||||
sm = {
|
|
||||||
'carState': MockCarState(vEgo=10.0, vCruise=20.0),
|
|
||||||
'radarState': MockRadarState(status=1.0),
|
|
||||||
'modelV2': MockModelData(valid=True),
|
|
||||||
'selfdriveState': MockSelfDriveState(experimentalMode=True),
|
|
||||||
}
|
|
||||||
return sm
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def mock_cp():
|
|
||||||
class CP:
|
|
||||||
radarUnavailable = False
|
|
||||||
return CP()
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def mock_mpc():
|
|
||||||
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
|
|
||||||
|
|
||||||
def test_initial_mode_is_acc(mock_cp, mock_mpc):
|
|
||||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
|
||||||
assert controller.mode() == "acc"
|
|
||||||
|
|
||||||
def test_standstill_triggers_blended(mock_cp, mock_mpc, default_sm):
|
|
||||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
|
||||||
default_sm['carState'].standstill = True
|
|
||||||
for _ in range(10):
|
|
||||||
controller.update(default_sm)
|
|
||||||
assert controller.mode() == "blended"
|
|
||||||
|
|
||||||
def test_emergency_blended_on_fcw(mock_cp, mock_mpc, default_sm):
|
|
||||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
|
||||||
mock_mpc.crash_cnt = 1 # simulate FCW
|
|
||||||
for _ in range(2):
|
|
||||||
controller.update(default_sm)
|
|
||||||
assert controller.mode() == "blended"
|
|
||||||
|
|
||||||
def test_radarless_slowdown_triggers_blended(mock_cp, mock_mpc, default_sm):
|
|
||||||
mock_cp.radarUnavailable = True
|
|
||||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
|
||||||
|
|
||||||
# Force conditions to simulate slowdown
|
|
||||||
controller._slow_down_filter = FakeKalman(value=1.0) # Ensure urgency triggers slowdown
|
|
||||||
controller._v_ego_kph = 35.0
|
|
||||||
default_sm['modelV2'] = MockModelData(valid=False) # Incomplete trajectory
|
|
||||||
|
|
||||||
for _ in range(3):
|
|
||||||
controller.update(default_sm)
|
|
||||||
|
|
||||||
assert controller.mode() == "blended"
|
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController, HysteresisSignal
|
||||||
|
|
||||||
|
|
||||||
|
class MockLeadOne:
|
||||||
|
def __init__(self, status=0.0, dRel=30.0, vRel=0.0):
|
||||||
|
self.status = status
|
||||||
|
self.dRel = dRel
|
||||||
|
self.vRel = vRel
|
||||||
|
|
||||||
|
|
||||||
|
class MockRadarState:
|
||||||
|
def __init__(self, status=0.0, dRel=30.0, vRel=0.0):
|
||||||
|
self.leadOne = MockLeadOne(status=status, dRel=dRel, vRel=vRel)
|
||||||
|
|
||||||
|
|
||||||
|
class MockCarState:
|
||||||
|
def __init__(self, vEgo=0.0, vCruise=0.0, standstill=False):
|
||||||
|
self.vEgo = vEgo
|
||||||
|
self.vCruise = vCruise
|
||||||
|
self.standstill = standstill
|
||||||
|
|
||||||
|
|
||||||
|
class MockAction:
|
||||||
|
def __init__(self, desiredAcceleration=0.0, shouldStop=False):
|
||||||
|
self.desiredAcceleration = desiredAcceleration
|
||||||
|
self.shouldStop = shouldStop
|
||||||
|
|
||||||
|
|
||||||
|
class MockModelData:
|
||||||
|
def __init__(self, valid=True, endpoint_x=200.0, orientation_valid=None, desired_acceleration=0.0, should_stop=False):
|
||||||
|
position_size = 33 if valid else 10
|
||||||
|
orientation_size = position_size if orientation_valid is None else (33 if orientation_valid else 10)
|
||||||
|
position_x = [0.0] * position_size
|
||||||
|
if position_x:
|
||||||
|
position_x[-1] = endpoint_x
|
||||||
|
self.position = type("Pos", (), {"x": position_x})()
|
||||||
|
self.orientation = type("Ori", (), {"x": [0.0] * orientation_size})()
|
||||||
|
self.acceleration = type("Accel", (), {"x": [0.0] * position_size})()
|
||||||
|
self.action = MockAction(desired_acceleration, should_stop)
|
||||||
|
|
||||||
|
|
||||||
|
class MockSelfDriveState:
|
||||||
|
def __init__(self, experimentalMode=False):
|
||||||
|
self.experimentalMode = experimentalMode
|
||||||
|
|
||||||
|
|
||||||
|
class MockParams:
|
||||||
|
def get_bool(self, name):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def default_sm():
|
||||||
|
sm = {
|
||||||
|
'carState': MockCarState(vEgo=10.0, vCruise=20.0),
|
||||||
|
'radarState': MockRadarState(status=1.0),
|
||||||
|
'modelV2': MockModelData(valid=True),
|
||||||
|
'selfdriveState': MockSelfDriveState(experimentalMode=True),
|
||||||
|
}
|
||||||
|
return sm
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_cp():
|
||||||
|
class CP:
|
||||||
|
radarUnavailable = False
|
||||||
|
return CP()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_mpc():
|
||||||
|
class MPC:
|
||||||
|
crash_cnt = 0
|
||||||
|
return MPC()
|
||||||
|
|
||||||
|
|
||||||
|
def test_initial_mode_is_acc(mock_cp, mock_mpc):
|
||||||
|
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||||
|
assert controller.mode() == "acc"
|
||||||
|
|
||||||
|
|
||||||
|
def test_standstill_triggers_blended(mock_cp, mock_mpc, default_sm):
|
||||||
|
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||||
|
default_sm['radarState'] = MockRadarState(status=0.0)
|
||||||
|
default_sm['carState'].standstill = True
|
||||||
|
for _ in range(20):
|
||||||
|
controller.update(default_sm)
|
||||||
|
assert controller.mode() == "blended"
|
||||||
|
|
||||||
|
|
||||||
|
def test_emergency_blended_on_fcw(mock_cp, mock_mpc, default_sm):
|
||||||
|
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||||
|
default_sm['radarState'] = MockRadarState(status=0.0)
|
||||||
|
mock_mpc.crash_cnt = 1
|
||||||
|
controller.update(default_sm)
|
||||||
|
assert controller.mode() == "blended"
|
||||||
|
|
||||||
|
|
||||||
|
def test_radarless_slowdown_triggers_blended(mock_cp, mock_mpc, default_sm):
|
||||||
|
mock_cp.radarUnavailable = True
|
||||||
|
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||||
|
default_sm['radarState'] = MockRadarState(status=0.0)
|
||||||
|
default_sm['modelV2'] = MockModelData(valid=True, endpoint_x=0.0)
|
||||||
|
|
||||||
|
controller.update(default_sm)
|
||||||
|
|
||||||
|
assert controller.mode() == "blended"
|
||||||
|
|
||||||
|
|
||||||
|
def test_valid_position_with_missing_orientation_can_trigger_slowdown(mock_cp, mock_mpc, default_sm):
|
||||||
|
mock_cp.radarUnavailable = True
|
||||||
|
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||||
|
default_sm['radarState'] = MockRadarState(status=0.0)
|
||||||
|
default_sm['modelV2'] = MockModelData(valid=True, endpoint_x=0.0, orientation_valid=False)
|
||||||
|
|
||||||
|
controller.update(default_sm)
|
||||||
|
|
||||||
|
assert controller._trajectory_valid
|
||||||
|
assert controller.mode() == "blended"
|
||||||
|
|
||||||
|
|
||||||
|
def test_incomplete_position_does_not_trigger_slowdown(mock_cp, mock_mpc, default_sm):
|
||||||
|
mock_cp.radarUnavailable = True
|
||||||
|
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||||
|
default_sm['radarState'] = MockRadarState(status=0.0)
|
||||||
|
default_sm['modelV2'] = MockModelData(valid=False, endpoint_x=0.0)
|
||||||
|
|
||||||
|
for _ in range(3):
|
||||||
|
controller.update(default_sm)
|
||||||
|
|
||||||
|
assert not controller._trajectory_valid
|
||||||
|
assert not controller._has_slow_down
|
||||||
|
assert controller.mode() == "acc"
|
||||||
|
|
||||||
|
|
||||||
|
def test_slowdown_hysteresis_prevents_threshold_chatter():
|
||||||
|
signal = HysteresisSignal(enter_threshold=0.5, exit_threshold=0.4, rise_rate=1.0, fall_rate=1.0)
|
||||||
|
|
||||||
|
assert signal.update(0.55)
|
||||||
|
assert signal.update(0.45)
|
||||||
|
assert not signal.update(0.35)
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_should_stop_triggers_blended_without_valid_trajectory(mock_cp, mock_mpc, default_sm):
|
||||||
|
mock_cp.radarUnavailable = True
|
||||||
|
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||||
|
default_sm['radarState'] = MockRadarState(status=0.0)
|
||||||
|
default_sm['modelV2'] = MockModelData(valid=False, should_stop=True)
|
||||||
|
|
||||||
|
controller.update(default_sm)
|
||||||
|
|
||||||
|
assert not controller._trajectory_valid
|
||||||
|
assert controller.mode() == "blended"
|
||||||
|
|
||||||
|
|
||||||
|
def test_radar_lead_keeps_acc_over_model_slowdown(mock_cp, mock_mpc, default_sm):
|
||||||
|
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||||
|
default_sm['radarState'] = MockRadarState(status=1.0)
|
||||||
|
default_sm['modelV2'] = MockModelData(valid=True, endpoint_x=0.0)
|
||||||
|
|
||||||
|
for _ in range(3):
|
||||||
|
controller.update(default_sm)
|
||||||
|
|
||||||
|
assert controller._has_slow_down
|
||||||
|
assert controller._has_radar_acc_lead
|
||||||
|
assert controller.mode() == "acc"
|
||||||
|
|
||||||
|
|
||||||
|
def test_far_radar_lead_allows_blended_until_acc_relevant(mock_cp, mock_mpc, default_sm):
|
||||||
|
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||||
|
default_sm['radarState'] = MockRadarState(status=1.0, dRel=120.0, vRel=0.0)
|
||||||
|
default_sm['modelV2'] = MockModelData(valid=True, endpoint_x=0.0)
|
||||||
|
|
||||||
|
controller.update(default_sm)
|
||||||
|
|
||||||
|
assert controller._has_lead_filtered
|
||||||
|
assert not controller._has_radar_acc_lead
|
||||||
|
assert controller.mode() == "blended"
|
||||||
|
|
||||||
|
|
||||||
|
def test_relevant_radar_lead_smoothly_returns_to_acc(mock_cp, mock_mpc, default_sm):
|
||||||
|
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||||
|
default_sm['radarState'] = MockRadarState(status=1.0, dRel=120.0, vRel=0.0)
|
||||||
|
default_sm['modelV2'] = MockModelData(valid=True, endpoint_x=0.0)
|
||||||
|
controller.update(default_sm)
|
||||||
|
assert controller.mode() == "blended"
|
||||||
|
|
||||||
|
default_sm['radarState'] = MockRadarState(status=1.0, dRel=45.0, vRel=0.0)
|
||||||
|
for _ in range(20):
|
||||||
|
controller.update(default_sm)
|
||||||
|
|
||||||
|
assert controller._has_radar_acc_lead
|
||||||
|
assert controller.mode() == "acc"
|
||||||
|
|
||||||
|
|
||||||
|
def test_closing_far_radar_lead_returns_to_acc(mock_cp, mock_mpc, default_sm):
|
||||||
|
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||||
|
default_sm['radarState'] = MockRadarState(status=1.0, dRel=120.0, vRel=-25.0)
|
||||||
|
default_sm['modelV2'] = MockModelData(valid=True, endpoint_x=0.0)
|
||||||
|
|
||||||
|
for _ in range(20):
|
||||||
|
controller.update(default_sm)
|
||||||
|
|
||||||
|
assert controller._has_radar_acc_lead
|
||||||
|
assert controller.mode() == "acc"
|
||||||
|
|
||||||
|
|
||||||
|
def test_radar_lead_keeps_acc_over_fcw_and_standstill(mock_cp, mock_mpc, default_sm):
|
||||||
|
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||||
|
default_sm['radarState'] = MockRadarState(status=1.0)
|
||||||
|
default_sm['carState'].standstill = True
|
||||||
|
default_sm['modelV2'] = MockModelData(valid=True, endpoint_x=0.0, should_stop=True)
|
||||||
|
mock_mpc.crash_cnt = 1
|
||||||
|
|
||||||
|
for _ in range(10):
|
||||||
|
controller.update(default_sm)
|
||||||
|
|
||||||
|
assert controller._has_lead_filtered
|
||||||
|
assert controller._has_mpc_fcw
|
||||||
|
assert controller.mode() == "acc"
|
||||||
|
|
||||||
|
|
||||||
|
def test_lead_flicker_hold_prevents_one_frame_mode_flip(mock_cp, mock_mpc, default_sm):
|
||||||
|
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||||
|
default_sm['radarState'] = MockRadarState(status=1.0)
|
||||||
|
controller.update(default_sm)
|
||||||
|
|
||||||
|
default_sm['radarState'] = MockRadarState(status=0.0)
|
||||||
|
default_sm['modelV2'] = MockModelData(valid=True, endpoint_x=0.0)
|
||||||
|
controller.update(default_sm)
|
||||||
|
|
||||||
|
assert controller._has_lead_filtered
|
||||||
|
assert controller.mode() == "acc"
|
||||||
@@ -9,6 +9,8 @@ from 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_personality.accel_controller import AccelController
|
||||||
|
from openpilot.sunnypilot.selfdrive.controls.lib.radar_distance.radar_distance import RadarDistanceController
|
||||||
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.smart_cruise_control.smart_cruise_control import SmartCruiseControl
|
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.smart_cruise_control import SmartCruiseControl
|
||||||
@@ -26,6 +28,8 @@ class LongitudinalPlannerSP:
|
|||||||
self.events_sp = EventsSP()
|
self.events_sp = EventsSP()
|
||||||
self.resolver = SpeedLimitResolver()
|
self.resolver = SpeedLimitResolver()
|
||||||
self.dec = DynamicExperimentalController(CP, mpc)
|
self.dec = DynamicExperimentalController(CP, mpc)
|
||||||
|
self.accel = AccelController(CP, mpc)
|
||||||
|
self.radar_distance = RadarDistanceController(CP)
|
||||||
self.scc = SmartCruiseControl()
|
self.scc = SmartCruiseControl()
|
||||||
self.resolver = SpeedLimitResolver()
|
self.resolver = SpeedLimitResolver()
|
||||||
self.sla = SpeedLimitAssist(CP, CP_SP)
|
self.sla = SpeedLimitAssist(CP, CP_SP)
|
||||||
@@ -76,8 +80,13 @@ class LongitudinalPlannerSP:
|
|||||||
def update(self, sm: messaging.SubMaster) -> None:
|
def update(self, sm: messaging.SubMaster) -> None:
|
||||||
self.events_sp.clear()
|
self.events_sp.clear()
|
||||||
self.dec.update(sm)
|
self.dec.update(sm)
|
||||||
|
self.accel.update(sm)
|
||||||
|
self.radar_distance.update(sm)
|
||||||
self.e2e_alerts_helper.update(sm, self.events_sp)
|
self.e2e_alerts_helper.update(sm, self.events_sp)
|
||||||
|
|
||||||
|
def smooth_radarstate(self, radarstate):
|
||||||
|
return self.radar_distance.smooth_radarstate(radarstate)
|
||||||
|
|
||||||
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')
|
||||||
|
|
||||||
@@ -138,4 +147,15 @@ class LongitudinalPlannerSP:
|
|||||||
e2eAlerts.greenLightAlert = self.e2e_alerts_helper.green_light_alert
|
e2eAlerts.greenLightAlert = self.e2e_alerts_helper.green_light_alert
|
||||||
e2eAlerts.leadDepartAlert = self.e2e_alerts_helper.lead_depart_alert
|
e2eAlerts.leadDepartAlert = self.e2e_alerts_helper.lead_depart_alert
|
||||||
|
|
||||||
|
# Acceleration Personality
|
||||||
|
acceleration = longitudinalPlanSP.acceleration
|
||||||
|
acceleration.personality = self.accel.personality()
|
||||||
|
acceleration.enabled = self.accel.enabled()
|
||||||
|
acceleration.maxAccel = float(self.accel.max_accel())
|
||||||
|
acceleration.brakeNeed = float(self.accel.brake_need())
|
||||||
|
acceleration.decelTarget = float(self.accel.decel_target())
|
||||||
|
acceleration.smoothActive = self.accel.smooth_active()
|
||||||
|
acceleration.bypassed = bool(self.accel.bypassed())
|
||||||
|
|
||||||
|
|
||||||
pm.send('longitudinalPlanSP', plan_sp_send)
|
pm.send('longitudinalPlanSP', plan_sp_send)
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
"""
|
||||||
|
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.
|
||||||
|
|
||||||
|
Radar Distance: hold a just-dropped, recently-sustained lead alive through radar flicker so the MPC does
|
||||||
|
not lose+regain it. Obstacle-monotone (held obstacle <= last real <= stock) -> braking is always >= stock,
|
||||||
|
never less. Wall-clock bounded, flicker-proof. Default off => stock passthrough.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from opendbc.car import structs
|
||||||
|
from openpilot.common.params import Params
|
||||||
|
from openpilot.common.realtime import DT_MDL
|
||||||
|
|
||||||
|
HOLD_MAX_FRAMES = 10 # ~0.5s cap, measured since the last SUSTAINED lead (not reset by 1-frame flicker)
|
||||||
|
SUSTAIN_FRAMES = 2 # consecutive valid frames to (re)arm and reset the wall-clock
|
||||||
|
DROPOUT_DREL = 1.0
|
||||||
|
MIN_PROB = 0.2
|
||||||
|
FCW_PROB_CAP = 0.9 # held lead can't reach the FCW gate (>0.9) -> no false FCW
|
||||||
|
MIN_HELD_DREL = 0.5
|
||||||
|
|
||||||
|
|
||||||
|
class _HeldLead:
|
||||||
|
__slots__ = ('status', 'dRel', 'yRel', 'vRel', 'vLead', 'vLeadK', 'aLeadK', 'aLeadTau', 'modelProb')
|
||||||
|
|
||||||
|
def __init__(self, dRel, vRel, vLead, aLeadK, aLeadTau, modelProb):
|
||||||
|
self.status = True
|
||||||
|
self.dRel = dRel
|
||||||
|
self.vRel = vRel
|
||||||
|
self.vLead = vLead
|
||||||
|
self.vLeadK = vLead
|
||||||
|
self.aLeadK = aLeadK
|
||||||
|
self.aLeadTau = aLeadTau
|
||||||
|
self.modelProb = modelProb
|
||||||
|
self.yRel = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
class _RadarStateProxy:
|
||||||
|
__slots__ = ('leadOne', 'leadTwo')
|
||||||
|
|
||||||
|
def __init__(self, lead_one, lead_two):
|
||||||
|
self.leadOne = lead_one
|
||||||
|
self.leadTwo = lead_two
|
||||||
|
|
||||||
|
|
||||||
|
class _LeadHold:
|
||||||
|
def __init__(self):
|
||||||
|
self._last = None
|
||||||
|
self._sustained = 0
|
||||||
|
self._since_real = 0
|
||||||
|
self._armed = False
|
||||||
|
self._held_dRel = 0.0
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.__init__()
|
||||||
|
|
||||||
|
def step(self, raw):
|
||||||
|
if raw.status and raw.dRel > DROPOUT_DREL and raw.modelProb > MIN_PROB:
|
||||||
|
self._last = (raw.dRel, raw.vRel, raw.vLead, raw.aLeadK, raw.aLeadTau, raw.modelProb)
|
||||||
|
self._sustained += 1
|
||||||
|
if self._sustained >= SUSTAIN_FRAMES:
|
||||||
|
self._since_real = 0
|
||||||
|
self._armed = True
|
||||||
|
return raw
|
||||||
|
|
||||||
|
self._sustained = 0
|
||||||
|
self._since_real += 1
|
||||||
|
if self._armed and self._last is not None and self._since_real <= HOLD_MAX_FRAMES:
|
||||||
|
dRel0, vRel0, vLead0, aLeadK0, aLeadTau0, prob0 = self._last
|
||||||
|
if self._since_real == 1:
|
||||||
|
self._held_dRel = dRel0
|
||||||
|
self._held_dRel = max(MIN_HELD_DREL, self._held_dRel - max(-vRel0, 0.0) * DT_MDL)
|
||||||
|
return _HeldLead(self._held_dRel, vRel0, vLead0, min(aLeadK0, 0.0), aLeadTau0, min(prob0, FCW_PROB_CAP))
|
||||||
|
|
||||||
|
self._armed = False
|
||||||
|
return raw
|
||||||
|
|
||||||
|
|
||||||
|
class RadarDistanceController:
|
||||||
|
def __init__(self, CP: structs.CarParams, params=None):
|
||||||
|
self._CP = CP
|
||||||
|
self._params = params or Params()
|
||||||
|
self._frame = 0
|
||||||
|
self._enabled = self._params.get_bool("RadarDistance")
|
||||||
|
self._one = _LeadHold()
|
||||||
|
self._two = _LeadHold()
|
||||||
|
|
||||||
|
def _read_params(self) -> None:
|
||||||
|
enabled = self._params.get_bool("RadarDistance")
|
||||||
|
if enabled and not self._enabled:
|
||||||
|
self._one.reset()
|
||||||
|
self._two.reset()
|
||||||
|
self._enabled = enabled
|
||||||
|
|
||||||
|
def update(self, sm) -> None:
|
||||||
|
if self._frame % int(1. / DT_MDL) == 0:
|
||||||
|
self._read_params()
|
||||||
|
self._frame += 1
|
||||||
|
|
||||||
|
def enabled(self) -> bool:
|
||||||
|
return self._enabled
|
||||||
|
|
||||||
|
def smooth_radarstate(self, radarstate):
|
||||||
|
if not self._enabled:
|
||||||
|
return radarstate
|
||||||
|
return _RadarStateProxy(self._one.step(radarstate.leadOne), self._two.step(radarstate.leadTwo))
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
"""
|
||||||
|
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||||
|
|
||||||
|
This file is part of sunnypilot and is licensed under the MIT License.
|
||||||
|
See the LICENSE.md file in the root directory for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from openpilot.sunnypilot.selfdrive.controls.lib.radar_distance.radar_distance import \
|
||||||
|
RadarDistanceController, HOLD_MAX_FRAMES, FCW_PROB_CAP
|
||||||
|
|
||||||
|
COMFORT_BRAKE = 2.5
|
||||||
|
|
||||||
|
|
||||||
|
class FakeParams:
|
||||||
|
def __init__(self, store=None):
|
||||||
|
self.store = dict(store or {})
|
||||||
|
|
||||||
|
def get_bool(self, key):
|
||||||
|
return bool(self.store.get(key, False))
|
||||||
|
|
||||||
|
|
||||||
|
def lead(status=True, dRel=40.0, vRel=-2.0, vLead=18.0, aLeadK=0.0, aLeadTau=1.5, modelProb=0.95):
|
||||||
|
return SimpleNamespace(status=status, dRel=dRel, yRel=0.0, vRel=vRel, vLead=vLead, vLeadK=vLead,
|
||||||
|
aLeadK=aLeadK, aLeadTau=aLeadTau, modelProb=modelProb)
|
||||||
|
|
||||||
|
|
||||||
|
def rs(one, two=None):
|
||||||
|
return SimpleNamespace(leadOne=one, leadTwo=two or lead(status=False, dRel=0.0, modelProb=0.0))
|
||||||
|
|
||||||
|
|
||||||
|
def obstacle(ld):
|
||||||
|
return ld.dRel + ld.vLead ** 2 / (2 * COMFORT_BRAKE)
|
||||||
|
|
||||||
|
|
||||||
|
def ctrl(enabled=True):
|
||||||
|
return RadarDistanceController(CP=SimpleNamespace(), params=FakeParams({'RadarDistance': enabled}))
|
||||||
|
|
||||||
|
|
||||||
|
def test_disabled_is_identity():
|
||||||
|
c = ctrl(enabled=False)
|
||||||
|
r = rs(lead())
|
||||||
|
assert c.smooth_radarstate(r) is r # byte-stock passthrough
|
||||||
|
|
||||||
|
|
||||||
|
def test_valid_lead_passthrough():
|
||||||
|
c = ctrl()
|
||||||
|
one = lead(dRel=40.0)
|
||||||
|
out = c.smooth_radarstate(rs(one))
|
||||||
|
assert out.leadOne is one
|
||||||
|
|
||||||
|
|
||||||
|
def test_holds_after_sustained_dropout():
|
||||||
|
c = ctrl()
|
||||||
|
for _ in range(3): # sustain (>= SUSTAIN_FRAMES)
|
||||||
|
c.smooth_radarstate(rs(lead(dRel=30.0, vRel=-4.0, vLead=16.0)))
|
||||||
|
out = c.smooth_radarstate(rs(lead(status=False, dRel=0.0, modelProb=0.0)))
|
||||||
|
held = out.leadOne
|
||||||
|
assert held.status is True
|
||||||
|
assert held.dRel < 30.0 # dead-reckoned closer
|
||||||
|
assert held.dRel == pytest.approx(30.0 - 4.0 * 0.05, abs=1e-6)
|
||||||
|
|
||||||
|
|
||||||
|
def test_obstacle_monotone_during_hold():
|
||||||
|
c = ctrl()
|
||||||
|
for _ in range(3):
|
||||||
|
c.smooth_radarstate(rs(lead(dRel=30.0, vRel=-4.0, vLead=16.0)))
|
||||||
|
last_obs = obstacle(lead(dRel=30.0, vLead=16.0))
|
||||||
|
prev = last_obs
|
||||||
|
for _ in range(HOLD_MAX_FRAMES):
|
||||||
|
held = c.smooth_radarstate(rs(lead(status=False, dRel=0.0, modelProb=0.0))).leadOne
|
||||||
|
if not held.status:
|
||||||
|
break
|
||||||
|
o = obstacle(held)
|
||||||
|
assert o <= last_obs + 1e-6 # never farther than the last real obstacle (brakes >= last real)
|
||||||
|
assert o <= prev + 1e-6 # monotonically non-increasing -> brakes more over the hold
|
||||||
|
prev = o
|
||||||
|
|
||||||
|
|
||||||
|
def test_releases_after_hold_cap():
|
||||||
|
c = ctrl()
|
||||||
|
for _ in range(3):
|
||||||
|
c.smooth_radarstate(rs(lead(dRel=30.0)))
|
||||||
|
statuses = [c.smooth_radarstate(rs(lead(status=False, dRel=0.0, modelProb=0.0))).leadOne.status
|
||||||
|
for _ in range(HOLD_MAX_FRAMES + 3)]
|
||||||
|
assert all(statuses[:HOLD_MAX_FRAMES]) # held through the cap
|
||||||
|
assert statuses[HOLD_MAX_FRAMES] is False # released after
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_hold_without_sustained_lead():
|
||||||
|
c = ctrl()
|
||||||
|
c.smooth_radarstate(rs(lead(dRel=30.0))) # single valid frame (< SUSTAIN_FRAMES)
|
||||||
|
out = c.smooth_radarstate(rs(lead(status=False, dRel=0.0, modelProb=0.0)))
|
||||||
|
assert out.leadOne.status is False # not armed -> no hold
|
||||||
|
|
||||||
|
|
||||||
|
def test_flicker_does_not_reset_wall_clock():
|
||||||
|
c = ctrl()
|
||||||
|
for _ in range(3):
|
||||||
|
c.smooth_radarstate(rs(lead(dRel=30.0)))
|
||||||
|
# 1/0/1/0 flicker: lone valid frames must NOT reset the wall-clock (sustained < SUSTAIN_FRAMES)
|
||||||
|
for _ in range(4):
|
||||||
|
c.smooth_radarstate(rs(lead(status=False, dRel=0.0, modelProb=0.0))) # dropout
|
||||||
|
c.smooth_radarstate(rs(lead(dRel=31.0))) # lone valid
|
||||||
|
assert c._one._since_real > 0 # wall-clock kept climbing through the flicker
|
||||||
|
|
||||||
|
|
||||||
|
def test_fcw_prob_capped_and_aleadk_not_positive():
|
||||||
|
c = ctrl()
|
||||||
|
for _ in range(3):
|
||||||
|
c.smooth_radarstate(rs(lead(dRel=30.0, aLeadK=1.0, modelProb=0.99)))
|
||||||
|
held = c.smooth_radarstate(rs(lead(status=False, dRel=0.0, modelProb=0.0))).leadOne
|
||||||
|
assert held.modelProb <= FCW_PROB_CAP # no false FCW from a held phantom
|
||||||
|
assert held.aLeadK <= 0.0 # never project the held lead as accelerating
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
"""
|
||||||
|
Copyright (c) 2021-, rav4kumar, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||||
|
|
||||||
|
This file is part of sunnypilot and is licensed under the MIT License.
|
||||||
|
See the LICENSE.md file in the root directory for more details.
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from openpilot.common.constants import CV
|
||||||
|
from openpilot.common.realtime import DT_MDL
|
||||||
|
from openpilot.common.params import Params
|
||||||
|
|
||||||
|
NEARSIDE_PROB = 0.25
|
||||||
|
EDGE_PROB = 0.35
|
||||||
|
EDGE_REACTION_TIME = 1.0
|
||||||
|
EDGE_CLEAR_TIME = 0.3
|
||||||
|
MIN_SPEED = 20 * CV.MPH_TO_MS
|
||||||
|
NEAR_EDGE_DISTANCE = 4.5
|
||||||
|
LEFT_NEARSIDE_LANE_IDX = 1
|
||||||
|
RIGHT_NEARSIDE_LANE_IDX = 2
|
||||||
|
|
||||||
|
|
||||||
|
class RoadEdgeLaneChangeController:
|
||||||
|
def __init__(self, desire_helper):
|
||||||
|
self.DH = desire_helper
|
||||||
|
self.params = Params()
|
||||||
|
self.enabled = self.params.get_bool("RoadEdgeLaneChangeEnabled")
|
||||||
|
self.param_read_counter = 0
|
||||||
|
self.left_edge_detected = False
|
||||||
|
self.right_edge_detected = False
|
||||||
|
self.left_edge_timer = 0.0
|
||||||
|
self.right_edge_timer = 0.0
|
||||||
|
self.left_clear_timer = 0.0
|
||||||
|
self.right_clear_timer = 0.0
|
||||||
|
|
||||||
|
def read_params(self) -> None:
|
||||||
|
self.enabled = self.params.get_bool("RoadEdgeLaneChangeEnabled")
|
||||||
|
|
||||||
|
def update_params(self) -> None:
|
||||||
|
if self.param_read_counter % 50 == 0:
|
||||||
|
self.read_params()
|
||||||
|
self.param_read_counter += 1
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
self.left_edge_detected = False
|
||||||
|
self.right_edge_detected = False
|
||||||
|
self.left_edge_timer = 0.0
|
||||||
|
self.right_edge_timer = 0.0
|
||||||
|
self.left_clear_timer = 0.0
|
||||||
|
self.right_clear_timer = 0.0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _road_edge_y(road_edges, idx: int) -> float | None:
|
||||||
|
if road_edges is None or len(road_edges) <= idx or len(road_edges[idx].y) == 0:
|
||||||
|
return None
|
||||||
|
return road_edges[idx].y[0]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _edge_is_near(edge_y: float | None, left: bool) -> bool:
|
||||||
|
if edge_y is None:
|
||||||
|
return False
|
||||||
|
if left:
|
||||||
|
return bool(-NEAR_EDGE_DISTANCE < edge_y < 0.0)
|
||||||
|
return bool(0.0 < edge_y < NEAR_EDGE_DISTANCE)
|
||||||
|
|
||||||
|
def update(self, road_edge_stds, lane_line_probs, v_ego: float, road_edges=None) -> None:
|
||||||
|
self.update_params()
|
||||||
|
|
||||||
|
if not self.enabled or v_ego < MIN_SPEED:
|
||||||
|
self.reset()
|
||||||
|
return
|
||||||
|
|
||||||
|
left_edge_prob = np.clip(1.0 - road_edge_stds[0], 0.0, 1.0)
|
||||||
|
right_edge_prob = np.clip(1.0 - road_edge_stds[1], 0.0, 1.0)
|
||||||
|
left_lane_prob = lane_line_probs[LEFT_NEARSIDE_LANE_IDX]
|
||||||
|
right_lane_prob = lane_line_probs[RIGHT_NEARSIDE_LANE_IDX]
|
||||||
|
|
||||||
|
left_edge_y = self._road_edge_y(road_edges, 0)
|
||||||
|
right_edge_y = self._road_edge_y(road_edges, 1)
|
||||||
|
left_edge_near = self._edge_is_near(left_edge_y, True)
|
||||||
|
right_edge_near = self._edge_is_near(right_edge_y, False)
|
||||||
|
|
||||||
|
left_cond = left_edge_prob > EDGE_PROB and (left_edge_near or (left_edge_y is None and left_lane_prob < NEARSIDE_PROB))
|
||||||
|
right_cond = right_edge_prob > EDGE_PROB and (right_edge_near or (right_edge_y is None and right_lane_prob < NEARSIDE_PROB))
|
||||||
|
|
||||||
|
if left_cond:
|
||||||
|
self.left_edge_timer = min(self.left_edge_timer + DT_MDL, EDGE_REACTION_TIME + EDGE_CLEAR_TIME)
|
||||||
|
self.left_clear_timer = 0.0
|
||||||
|
if self.left_edge_timer > EDGE_REACTION_TIME:
|
||||||
|
self.left_edge_detected = True
|
||||||
|
else:
|
||||||
|
self.left_clear_timer += DT_MDL
|
||||||
|
if self.left_clear_timer > EDGE_CLEAR_TIME:
|
||||||
|
self.left_edge_timer = 0.0
|
||||||
|
self.left_edge_detected = False
|
||||||
|
|
||||||
|
if right_cond:
|
||||||
|
self.right_edge_timer = min(self.right_edge_timer + DT_MDL, EDGE_REACTION_TIME + EDGE_CLEAR_TIME)
|
||||||
|
self.right_clear_timer = 0.0
|
||||||
|
if self.right_edge_timer > EDGE_REACTION_TIME:
|
||||||
|
self.right_edge_detected = True
|
||||||
|
else:
|
||||||
|
self.right_clear_timer += DT_MDL
|
||||||
|
if self.right_clear_timer > EDGE_CLEAR_TIME:
|
||||||
|
self.right_edge_timer = 0.0
|
||||||
|
self.right_edge_detected = False
|
||||||
@@ -5,6 +5,8 @@ from openpilot.common.params import Params
|
|||||||
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
|
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.lane_turn_desire import LaneTurnController, LANE_CHANGE_SPEED_MIN
|
from openpilot.sunnypilot.selfdrive.controls.lib.lane_turn_desire import LaneTurnController, LANE_CHANGE_SPEED_MIN
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeMode
|
from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeMode
|
||||||
|
from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController
|
||||||
|
|
||||||
|
|
||||||
TurnDirection = custom.ModelDataV2SP.TurnDirection
|
TurnDirection = custom.ModelDataV2SP.TurnDirection
|
||||||
|
|
||||||
@@ -107,7 +109,11 @@ def set_lane_turn_params():
|
|||||||
])
|
])
|
||||||
def test_desire_helper_integration(carstate, lateral_active, lane_change_prob, expected_desire, set_lane_turn_params):
|
def test_desire_helper_integration(carstate, lateral_active, lane_change_prob, expected_desire, set_lane_turn_params):
|
||||||
dh = DesireHelper()
|
dh = DesireHelper()
|
||||||
|
relc = RoadEdgeLaneChangeController(dh)
|
||||||
|
relc.enabled = True
|
||||||
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE
|
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE
|
||||||
for _ in range(10):
|
for _ in range(10):
|
||||||
dh.update(carstate, lateral_active, lane_change_prob)
|
dh.update(carstate, lateral_active, lane_change_prob,
|
||||||
|
left_edge_detected=relc.left_edge_detected, right_edge_detected=relc.right_edge_detected)
|
||||||
assert dh.desire == expected_desire # The first four tests were unit tests to test the controller, where this tests the integration in desire helpers
|
assert dh.desire == expected_desire # The first four tests were unit tests to test the controller, where this tests the integration in desire helpers
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
"""
|
||||||
|
Copyright (c) 2021-, rav4kumar, 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 pytest
|
||||||
|
|
||||||
|
from openpilot.common.realtime import DT_MDL
|
||||||
|
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
|
||||||
|
from openpilot.sunnypilot.selfdrive.controls.lib.relc import (
|
||||||
|
RoadEdgeLaneChangeController, EDGE_REACTION_TIME, EDGE_CLEAR_TIME, MIN_SPEED,
|
||||||
|
)
|
||||||
|
|
||||||
|
V_HIGH = MIN_SPEED + 2.0
|
||||||
|
V_LOW = MIN_SPEED - 1.0
|
||||||
|
|
||||||
|
|
||||||
|
class DummyRoadEdge:
|
||||||
|
def __init__(self, y):
|
||||||
|
self.y = [y]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def relc(mocker):
|
||||||
|
mock_params = mocker.patch("openpilot.sunnypilot.selfdrive.controls.lib.relc.Params")
|
||||||
|
mock_params.return_value.get_bool.return_value = True
|
||||||
|
controller = RoadEdgeLaneChangeController(DesireHelper())
|
||||||
|
controller.enabled = True
|
||||||
|
return controller
|
||||||
|
|
||||||
|
|
||||||
|
def make_road_edges(left_y=-3.0, right_y=3.0):
|
||||||
|
return [DummyRoadEdge(left_y), DummyRoadEdge(right_y)]
|
||||||
|
|
||||||
|
|
||||||
|
def drive(controller, road_edge_stds, lane_line_probs, seconds, v_ego=V_HIGH, road_edges=None):
|
||||||
|
for _ in range(int(seconds / DT_MDL) + 1):
|
||||||
|
controller.update(road_edge_stds, lane_line_probs, v_ego, road_edges)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("road_edge_stds,lane_line_probs,attr", [
|
||||||
|
([0.0, 0.9], [0.8, 0.0, 0.8, 0.8], "left_edge_detected"),
|
||||||
|
([0.9, 0.0], [0.8, 0.8, 0.0, 0.8], "right_edge_detected"),
|
||||||
|
])
|
||||||
|
def test_edge_detection(relc, road_edge_stds, lane_line_probs, attr):
|
||||||
|
drive(relc, road_edge_stds, lane_line_probs, EDGE_REACTION_TIME + 0.1)
|
||||||
|
assert getattr(relc, attr)
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_detection_requires_time(relc):
|
||||||
|
drive(relc, [0.0, 0.9], [0.8, 0.0, 0.8, 0.8], EDGE_REACTION_TIME - 0.05)
|
||||||
|
assert not relc.left_edge_detected
|
||||||
|
|
||||||
|
|
||||||
|
def test_both_edges_detected(relc):
|
||||||
|
drive(relc, [0.0, 0.0], [0.8, 0.0, 0.0, 0.8], EDGE_REACTION_TIME + 0.1)
|
||||||
|
assert relc.left_edge_detected
|
||||||
|
assert relc.right_edge_detected
|
||||||
|
|
||||||
|
|
||||||
|
def test_noise_doesnt_clear(relc):
|
||||||
|
edge = ([0.0, 0.9], [0.8, 0.0, 0.8, 0.8])
|
||||||
|
clear = ([0.9, 0.9], [0.8, 0.8, 0.8, 0.8])
|
||||||
|
|
||||||
|
drive(relc, *edge, EDGE_REACTION_TIME + 0.1)
|
||||||
|
assert relc.left_edge_detected
|
||||||
|
|
||||||
|
relc.update(*clear, V_HIGH)
|
||||||
|
relc.update(*edge, V_HIGH)
|
||||||
|
assert relc.left_edge_detected
|
||||||
|
|
||||||
|
|
||||||
|
def test_clears_after_window(relc):
|
||||||
|
edge = ([0.0, 0.9], [0.8, 0.0, 0.8, 0.8])
|
||||||
|
clear = ([0.9, 0.9], [0.8, 0.8, 0.8, 0.8])
|
||||||
|
|
||||||
|
drive(relc, *edge, EDGE_REACTION_TIME + 0.1)
|
||||||
|
assert relc.left_edge_detected
|
||||||
|
|
||||||
|
drive(relc, *clear, EDGE_CLEAR_TIME + 0.05)
|
||||||
|
assert not relc.left_edge_detected
|
||||||
|
assert relc.left_edge_timer == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_low_speed_skips(relc):
|
||||||
|
drive(relc, [0.0, 0.9], [0.8, 0.0, 0.8, 0.8], EDGE_REACTION_TIME + 0.1, v_ego=V_LOW)
|
||||||
|
assert not relc.left_edge_detected
|
||||||
|
assert relc.left_edge_timer == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_speed_drop_resets(relc):
|
||||||
|
drive(relc, [0.0, 0.9], [0.8, 0.0, 0.8, 0.8], EDGE_REACTION_TIME + 0.1)
|
||||||
|
assert relc.left_edge_detected
|
||||||
|
|
||||||
|
relc.update([0.0, 0.9], [0.8, 0.0, 0.8, 0.8], V_LOW)
|
||||||
|
assert not relc.left_edge_detected
|
||||||
|
|
||||||
|
|
||||||
|
def test_param_off_resets(relc):
|
||||||
|
drive(relc, [0.0, 0.9], [0.8, 0.0, 0.8, 0.8], EDGE_REACTION_TIME + 0.1)
|
||||||
|
assert relc.left_edge_detected
|
||||||
|
|
||||||
|
relc.params.get_bool.return_value = False
|
||||||
|
relc.read_params()
|
||||||
|
relc.update([0.0, 0.9], [0.8, 0.0, 0.8, 0.8], V_HIGH)
|
||||||
|
assert not relc.left_edge_detected
|
||||||
|
assert not relc.right_edge_detected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("lane_line_probs", [
|
||||||
|
[0.0, 0.8, 0.8, 0.8],
|
||||||
|
[0.8, 0.8, 0.8, 0.0],
|
||||||
|
])
|
||||||
|
def test_outer_lane_lines_do_not_drive_edge_detection(relc, lane_line_probs):
|
||||||
|
drive(relc, [0.0, 0.0], lane_line_probs, EDGE_REACTION_TIME + 0.1)
|
||||||
|
assert not relc.left_edge_detected
|
||||||
|
assert not relc.right_edge_detected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("road_edge_stds,road_edges,attr", [
|
||||||
|
([0.0, 0.9], make_road_edges(left_y=-3.0, right_y=8.0), "left_edge_detected"),
|
||||||
|
([0.9, 0.0], make_road_edges(left_y=-8.0, right_y=3.0), "right_edge_detected"),
|
||||||
|
])
|
||||||
|
def test_near_road_edge_geometry_blocks_with_visible_lane_lines(relc, road_edge_stds, road_edges, attr):
|
||||||
|
drive(relc, road_edge_stds, [0.8, 0.8, 0.8, 0.8], EDGE_REACTION_TIME + 0.1, road_edges=road_edges)
|
||||||
|
assert getattr(relc, attr)
|
||||||
|
|
||||||
|
|
||||||
|
def test_far_road_edge_geometry_does_not_block(relc):
|
||||||
|
drive(relc, [0.0, 0.0], [0.8, 0.0, 0.0, 0.8], EDGE_REACTION_TIME + 0.1, road_edges=make_road_edges(left_y=-8.0, right_y=8.0))
|
||||||
|
assert not relc.left_edge_detected
|
||||||
|
assert not relc.right_edge_detected
|
||||||
@@ -243,4 +243,12 @@ EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
|||||||
AlertStatus.normal, AlertSize.none,
|
AlertStatus.normal, AlertSize.none,
|
||||||
Priority.MID, VisualAlert.none, AudibleAlert.prompt, 3.),
|
Priority.MID, VisualAlert.none, AudibleAlert.prompt, 3.),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
EventNameSP.laneChangeRoadEdge: {
|
||||||
|
ET.WARNING: Alert(
|
||||||
|
"Lane Change Unavailable: Road Edge",
|
||||||
|
"",
|
||||||
|
AlertStatus.userPrompt, AlertSize.small,
|
||||||
|
Priority.LOW, VisualAlert.none, AudibleAlert.prompt, 0.1),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"AccelPersonality": {
|
||||||
|
"title": "Acceleration Profile",
|
||||||
|
"description": "Eco accelerates gently and brakes early and soft; Sport accelerates briskly. Hard-braking authority is always preserved."
|
||||||
|
},
|
||||||
|
"AccelPersonalityEnabled": {
|
||||||
|
"title": "Enable Acceleration Profiles",
|
||||||
|
"description": "Enable Eco/Normal/Sport acceleration profiles, including early soft braking."
|
||||||
|
},
|
||||||
"AccessToken": {
|
"AccessToken": {
|
||||||
"title": "AccessTokenIsNice",
|
"title": "AccessTokenIsNice",
|
||||||
"description": ""
|
"description": ""
|
||||||
@@ -1098,6 +1106,10 @@
|
|||||||
"title": "Quiet Mode",
|
"title": "Quiet Mode",
|
||||||
"description": ""
|
"description": ""
|
||||||
},
|
},
|
||||||
|
"RadarDistance": {
|
||||||
|
"title": "Radar Distance",
|
||||||
|
"description": "Holds a lead through brief radar flicker/dropout so sunnypilot does not lose and re-grab it, smoothing the hard/late brakes that radar drop-outs cause. Braking is never reduced below stock."
|
||||||
|
},
|
||||||
"RainbowMode": {
|
"RainbowMode": {
|
||||||
"title": "Rainbow Mode",
|
"title": "Rainbow Mode",
|
||||||
"description": ""
|
"description": ""
|
||||||
@@ -1118,6 +1130,10 @@
|
|||||||
"title": "Record Front Lock",
|
"title": "Record Front Lock",
|
||||||
"description": ""
|
"description": ""
|
||||||
},
|
},
|
||||||
|
"RoadEdgeLaneChangeEnabled": {
|
||||||
|
"title": "Block Lane Change: Road Edge Detection",
|
||||||
|
"description": "Blocks lane changes when the model sees a road edge on the signaled side."
|
||||||
|
},
|
||||||
"RoadName": {
|
"RoadName": {
|
||||||
"title": "Road Name",
|
"title": "Road Name",
|
||||||
"description": ""
|
"description": ""
|
||||||
@@ -1324,14 +1340,30 @@
|
|||||||
"step": 0.1,
|
"step": 0.1,
|
||||||
"unit": "m/s\u00b2"
|
"unit": "m/s\u00b2"
|
||||||
},
|
},
|
||||||
|
"ToyotaAutoHold": {
|
||||||
|
"title": "Toyota: Auto Brake Hold FOR TSS2 HYBRID CARS",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"ToyotaDriveMode": {
|
||||||
|
"title": "Enable drive mode btn link",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
"ToyotaEnforceStockLongitudinal": {
|
"ToyotaEnforceStockLongitudinal": {
|
||||||
"title": "Toyota: Enforce Factory Longitudinal Control",
|
"title": "Toyota: Enforce Factory Longitudinal Control",
|
||||||
"description": "When enabled, sunnypilot will not take over control of gas and brakes. Factory Toyota longitudinal control will be used."
|
"description": "When enabled, sunnypilot will not take over control of gas and brakes. Factory Toyota longitudinal control will be used."
|
||||||
},
|
},
|
||||||
|
"ToyotaEnhancedBsm": {
|
||||||
|
"title": "Toyota: Prius TSS2 BSM and some tssp",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
"ToyotaStopAndGoHack": {
|
"ToyotaStopAndGoHack": {
|
||||||
"title": "Toyota: Stop and Go Hack (Alpha)",
|
"title": "Toyota: Stop and Go Hack (Alpha)",
|
||||||
"description": "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."
|
"description": "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."
|
||||||
},
|
},
|
||||||
|
"ToyotaTSS2Long": {
|
||||||
|
"title": "Toyota: custom longitudinal for TSS2",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
"TrainingVersion": {
|
"TrainingVersion": {
|
||||||
"title": "Training Version",
|
"title": "Training Version",
|
||||||
"description": ""
|
"description": ""
|
||||||
|
|||||||
@@ -537,6 +537,12 @@
|
|||||||
"value": 0
|
"value": 0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "RoadEdgeLaneChangeEnabled",
|
||||||
|
"widget": "toggle",
|
||||||
|
"title": "Block Lane Change: Road Edge Detection",
|
||||||
|
"description": "Blocks the lane change if the model sees a road edge on your signaled side."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -587,6 +593,26 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"key": "RadarDistance",
|
||||||
|
"widget": "toggle",
|
||||||
|
"title": "Radar Distance",
|
||||||
|
"description": "Holds a lead through brief radar flicker/dropout so sunnypilot does not lose and re-grab it, smoothing the hard/late brakes that radar drop-outs cause. Braking is never reduced below stock.",
|
||||||
|
"visibility": [
|
||||||
|
{
|
||||||
|
"type": "capability",
|
||||||
|
"field": "has_longitudinal_control",
|
||||||
|
"equals": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"enablement": [
|
||||||
|
{
|
||||||
|
"type": "capability",
|
||||||
|
"field": "has_longitudinal_control",
|
||||||
|
"equals": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"key": "DisengageOnAccelerator",
|
"key": "DisengageOnAccelerator",
|
||||||
"widget": "toggle",
|
"widget": "toggle",
|
||||||
@@ -620,6 +646,58 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"key": "AccelPersonalityEnabled",
|
||||||
|
"widget": "toggle",
|
||||||
|
"title": "Enable Acceleration Profiles",
|
||||||
|
"description": "Enables Eco/Normal/Sport acceleration profiles for longitudinal control, including early soft braking.",
|
||||||
|
"visibility": [
|
||||||
|
{
|
||||||
|
"type": "capability",
|
||||||
|
"field": "has_longitudinal_control",
|
||||||
|
"equals": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"enablement": [
|
||||||
|
{
|
||||||
|
"type": "capability",
|
||||||
|
"field": "has_longitudinal_control",
|
||||||
|
"equals": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "AccelPersonality",
|
||||||
|
"widget": "multiple_button",
|
||||||
|
"title": "Acceleration Profile",
|
||||||
|
"description": "Eco accelerates gently and brakes early and soft; Sport accelerates briskly. Hard-braking authority is always preserved.",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"value": 0,
|
||||||
|
"label": "Eco"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 1,
|
||||||
|
"label": "Normal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 2,
|
||||||
|
"label": "Sport"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"enablement": [
|
||||||
|
{
|
||||||
|
"type": "capability",
|
||||||
|
"field": "has_longitudinal_control",
|
||||||
|
"equals": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "param",
|
||||||
|
"key": "AccelPersonalityEnabled",
|
||||||
|
"equals": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"key": "IntelligentCruiseButtonManagement",
|
"key": "IntelligentCruiseButtonManagement",
|
||||||
"widget": "toggle",
|
"widget": "toggle",
|
||||||
@@ -2001,6 +2079,22 @@
|
|||||||
"equals": true
|
"equals": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "PlanplusControl",
|
||||||
|
"widget": "option",
|
||||||
|
"title": "Plan Plus Controls",
|
||||||
|
"description": "Adjust planplus model recentering strength. The higher this number the more aggressively the model will recover to lane center; too high and it will ping-pong.",
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 2.0,
|
||||||
|
"step": 0.1,
|
||||||
|
"enablement": [
|
||||||
|
{
|
||||||
|
"type": "param",
|
||||||
|
"key": "ShowAdvancedControls",
|
||||||
|
"equals": true
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -2168,6 +2262,50 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -24,6 +24,15 @@ sections:
|
|||||||
- $ref: '#/macros/longitudinal'
|
- $ref: '#/macros/longitudinal'
|
||||||
enablement:
|
enablement:
|
||||||
- $ref: '#/macros/longitudinal'
|
- $ref: '#/macros/longitudinal'
|
||||||
|
- key: RadarDistance
|
||||||
|
widget: toggle
|
||||||
|
title: Radar Distance
|
||||||
|
description: Holds a lead through brief radar flicker/dropout so sunnypilot does not lose and re-grab
|
||||||
|
it, smoothing the hard/late brakes that radar drop-outs cause. Braking is never reduced below stock.
|
||||||
|
visibility:
|
||||||
|
- $ref: '#/macros/longitudinal'
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/longitudinal'
|
||||||
- key: DisengageOnAccelerator
|
- key: DisengageOnAccelerator
|
||||||
widget: toggle
|
widget: toggle
|
||||||
title: Disengage Cruise on Accelerator Pedal
|
title: Disengage Cruise on Accelerator Pedal
|
||||||
@@ -43,6 +52,31 @@ sections:
|
|||||||
label: Relaxed
|
label: Relaxed
|
||||||
enablement:
|
enablement:
|
||||||
- $ref: '#/macros/longitudinal'
|
- $ref: '#/macros/longitudinal'
|
||||||
|
- key: AccelPersonalityEnabled
|
||||||
|
widget: toggle
|
||||||
|
title: Enable Acceleration Profiles
|
||||||
|
description: Enables Eco/Normal/Sport acceleration profiles for longitudinal control, including early soft braking.
|
||||||
|
visibility:
|
||||||
|
- $ref: '#/macros/longitudinal'
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/longitudinal'
|
||||||
|
- key: AccelPersonality
|
||||||
|
widget: multiple_button
|
||||||
|
title: Acceleration Profile
|
||||||
|
description: Eco accelerates gently and brakes early and soft; Sport accelerates briskly. Hard-braking
|
||||||
|
authority is always preserved.
|
||||||
|
options:
|
||||||
|
- value: 0
|
||||||
|
label: Eco
|
||||||
|
- value: 1
|
||||||
|
label: Normal
|
||||||
|
- value: 2
|
||||||
|
label: Sport
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/longitudinal'
|
||||||
|
- type: param
|
||||||
|
key: AccelPersonalityEnabled
|
||||||
|
equals: true
|
||||||
- key: IntelligentCruiseButtonManagement
|
- key: IntelligentCruiseButtonManagement
|
||||||
widget: toggle
|
widget: toggle
|
||||||
title: Intelligent Cruise Button Management (ICBM) (Alpha)
|
title: Intelligent Cruise Button Management (ICBM) (Alpha)
|
||||||
|
|||||||
@@ -51,6 +51,16 @@ sections:
|
|||||||
key: LagdToggle
|
key: LagdToggle
|
||||||
equals: true
|
equals: true
|
||||||
- $ref: '#/macros/advanced_only'
|
- $ref: '#/macros/advanced_only'
|
||||||
|
- key: PlanplusControl
|
||||||
|
widget: option
|
||||||
|
title: Plan Plus Controls
|
||||||
|
description: Adjust planplus model recentering strength. The higher this number the more aggressively the model will recover
|
||||||
|
to lane center; too high and it will ping-pong.
|
||||||
|
min: 0.0
|
||||||
|
max: 2.0
|
||||||
|
step: 0.1
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/advanced_only'
|
||||||
- id: lateral_control
|
- id: lateral_control
|
||||||
title: Lateral Control
|
title: Lateral Control
|
||||||
description: Neural network lateral control for supported models
|
description: Neural network lateral control for supported models
|
||||||
|
|||||||
@@ -255,3 +255,7 @@ sections:
|
|||||||
key: AutoLaneChangeTimer
|
key: AutoLaneChangeTimer
|
||||||
op: '>'
|
op: '>'
|
||||||
value: 0
|
value: 0
|
||||||
|
- key: RoadEdgeLaneChangeEnabled
|
||||||
|
widget: toggle
|
||||||
|
title: 'Block Lane Change: Road Edge Detection'
|
||||||
|
description: Blocks the lane change if the model sees a road edge on your signaled side.
|
||||||
|
|||||||
@@ -60,6 +60,30 @@ 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
|
||||||
|
|||||||
@@ -45,8 +45,9 @@ class ScrollState(Enum):
|
|||||||
|
|
||||||
|
|
||||||
class GuiScrollPanel2:
|
class GuiScrollPanel2:
|
||||||
def __init__(self, horizontal: bool = True) -> None:
|
def __init__(self, horizontal: bool = True, handle_out_of_bounds: 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
|
||||||
@@ -98,7 +99,7 @@ class GuiScrollPanel2:
|
|||||||
# simple exponential return if out of bounds
|
# simple exponential return if out of bounds
|
||||||
# out of bounds is handled by snapping, so skip if set
|
# out of bounds is handled by snapping, so skip if set
|
||||||
out_of_bounds = self.get_offset() > max_offset or self.get_offset() < min_offset
|
out_of_bounds = self.get_offset() > max_offset or self.get_offset() < min_offset
|
||||||
if out_of_bounds and snap_target is None:
|
if out_of_bounds and snap_target is None and self._handle_out_of_bounds:
|
||||||
target = max_offset if self.get_offset() > max_offset else min_offset
|
target = max_offset if self.get_offset() > max_offset else min_offset
|
||||||
|
|
||||||
dt = rl.get_frame_time() or 1e-6
|
dt = rl.get_frame_time() or 1e-6
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ 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
|
||||||
|
|
||||||
@@ -191,8 +190,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:
|
||||||
closest_delta_pos = min((((item.rect.x + item.rect.width / 2) - center_pos) for item in visible_items), key=abs)
|
center_pos = self._rect.x + self._rect.width / 2
|
||||||
|
closest_delta_pos = min((((item.rect.x + item.rect.width / 2) - center_pos) for item in visible_items), key=abs)
|
||||||
|
else:
|
||||||
|
center_pos = self._rect.y + self._rect.height / 2
|
||||||
|
closest_delta_pos = min((((item.rect.y + item.rect.height / 2) - 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)
|
||||||
|
|||||||
Reference in New Issue
Block a user