I cannot stop planning long

This commit is contained in:
firestar5683
2026-05-10 00:49:34 -05:00
parent ef44741760
commit c1e3525929
16 changed files with 692 additions and 132 deletions
@@ -9,31 +9,9 @@ from opendbc.car.tesla.preap.stock_cc_spoofer import StockCCSpoofer
from opendbc.car.tesla.values import CANBUS, CAR, CarControllerParams
from opendbc.car.vehicle_model import VehicleModel
_PREAP_RATE_LIMIT_BP = [0., 5., 15.]
_PREAP_RATE_LIMIT_UP = [5., 0.8, 0.15]
_PREAP_RATE_LIMIT_DOWN = [5., 3.5, 0.4]
_PREAP_EPS_ANGLE_ERROR_MAX = 20.0
def _apply_preap_steer_angle_limits(desired_angle: float, last_angle: float, v_ego: float,
steering_angle: float, lat_active: bool) -> float:
if not lat_active:
return steering_angle
steer_up = (last_angle * desired_angle > 0.) and (abs(desired_angle) > abs(last_angle))
rate_limit = _PREAP_RATE_LIMIT_UP if steer_up else _PREAP_RATE_LIMIT_DOWN
max_angle_diff = float(np.interp(v_ego, _PREAP_RATE_LIMIT_BP, rate_limit))
apply_angle = float(np.clip(desired_angle, last_angle - max_angle_diff, last_angle + max_angle_diff))
# Match the older Tesla unity controller and stay close to measured EPAS angle to avoid rejection.
return float(np.clip(apply_angle,
steering_angle - _PREAP_EPS_ANGLE_ERROR_MAX,
steering_angle + _PREAP_EPS_ANGLE_ERROR_MAX))
def get_safety_CP():
from opendbc.car.tesla.interface import CarInterface
return CarInterface.get_non_essential_params(CAR.TESLA_MODEL_S_PREAP if getattr(get_safety_CP, "_preap", False) else CAR.TESLA_MODEL_Y)
return CarInterface.get_non_essential_params(CAR.TESLA_MODEL_Y)
class CarController(CarControllerBase):
@@ -45,15 +23,15 @@ class CarController(CarControllerBase):
self.preap_long = None
self.stock_cc = None
# Vehicle model used for lateral limiting
self.VM = VehicleModel(get_safety_CP())
if CP.carFingerprint == CAR.TESLA_MODEL_S_PREAP:
get_safety_CP._preap = True
self.tesla_can = init_preap_can(dbc_names)
self.preap_long = PreAPLongController()
self.stock_cc = StockCCSpoofer()
# Vehicle model used for lateral limiting
self.VM = VehicleModel(get_safety_CP())
get_safety_CP._preap = False
from opendbc.car.tesla.interface import CarInterface
self.VM = VehicleModel(CarInterface.get_non_essential_params(CAR.TESLA_MODEL_S_PREAP))
def update(self, CC, CS, now_nanos, starpilot_toggles):
if self.CP.carFingerprint == CAR.TESLA_MODEL_S_PREAP:
@@ -117,8 +95,9 @@ class CarController(CarControllerBase):
CS.engagement.pedal_speed_kph = 0.0
if self.frame % 2 == 0:
self.apply_angle_last = _apply_preap_steer_angle_limits(
actuators.steeringAngleDeg, self.apply_angle_last, CS.out.vEgo, CS.out.steeringAngleDeg, lat_active,
self.apply_angle_last = apply_steer_angle_limits_vm(
actuators.steeringAngleDeg, self.apply_angle_last, CS.out.vEgoRaw, CS.out.steeringAngleDeg,
lat_active, CarControllerParams, self.VM,
)
cntr = (self.frame // 2) % 16
can_sends.append(self.tesla_can.create_steering_control(cntr, self.apply_angle_last, lat_active))
+28
View File
@@ -7,6 +7,13 @@ VISION_LEAD_TRACK_MIN_DISTANCE = 25.0
VISION_LEAD_TRACK_BASE_TIME_GAP = 1.75
VISION_LEAD_TRACK_CLOSING_GAIN = 0.20
VISION_LEAD_TRACK_CLOSING_CAP = 2.50
RADARLESS_MATCHED_FOLLOW_MIN_SPEED = 22.0
RADARLESS_MATCHED_FOLLOW_MAX_REL_SPEED = 2.0
RADARLESS_MATCHED_FOLLOW_MIN_HEADWAY = 0.95
RADARLESS_MATCHED_FOLLOW_HEADWAY_BELOW_TARGET = 0.35
RADARLESS_MATCHED_FOLLOW_HEADWAY_ABOVE_TARGET = 0.90
RADARLESS_MATCHED_FOLLOW_MAX_LEAD_BRAKE = 0.35
RADARLESS_MATCHED_FOLLOW_MIN_MODEL_PROB = 0.70
def should_track_lead(lead_status: bool, lead_distance: float, model_length: float, stop_distance: float,
@@ -26,6 +33,27 @@ def should_track_lead(lead_status: bool, lead_distance: float, model_length: flo
return float(lead_distance) < min(model_limit, vision_limit)
def is_radarless_matched_follow_window(v_ego: float, lead_distance: float, v_lead: float, t_follow: float, *,
radar: bool = False, lead_brake: float = 0.0,
lead_prob: float = 0.0) -> bool:
if radar or float(t_follow) <= 0.0 or float(v_ego) < RADARLESS_MATCHED_FOLLOW_MIN_SPEED:
return False
if float(lead_prob) < RADARLESS_MATCHED_FOLLOW_MIN_MODEL_PROB:
return False
if float(lead_brake) > RADARLESS_MATCHED_FOLLOW_MAX_LEAD_BRAKE:
return False
relative_speed = float(v_ego) - float(v_lead)
if abs(relative_speed) > RADARLESS_MATCHED_FOLLOW_MAX_REL_SPEED:
return False
actual_headway = float(lead_distance) / max(float(v_ego), 1e-3)
min_headway = max(RADARLESS_MATCHED_FOLLOW_MIN_HEADWAY,
float(t_follow) - RADARLESS_MATCHED_FOLLOW_HEADWAY_BELOW_TARGET)
max_headway = float(t_follow) + RADARLESS_MATCHED_FOLLOW_HEADWAY_ABOVE_TARGET
return min_headway <= actual_headway <= max_headway
def get_tracked_lead_catchup_bias(v_ego: float, lead_distance: float, desired_gap: float, closing_speed: float,
v_cruise: float | None = None) -> float:
gap_error = lead_distance - desired_gap
@@ -13,7 +13,7 @@ from openpilot.common.constants import CV
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.realtime import DT_MDL
from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.controls.lib.lead_behavior import get_tracked_lead_catchup_bias
from openpilot.selfdrive.controls.lib.lead_behavior import get_tracked_lead_catchup_bias, is_radarless_matched_follow_window
# WARNING: imports outside of constants will not trigger a rebuild
from openpilot.selfdrive.modeld.constants import index_function
@@ -77,6 +77,8 @@ FAR_RADAR_LEAD_ACCEL_TAPER_MIN_GAP_EXCESS = 8.0
FAR_RADAR_LEAD_ACCEL_TAPER_MIN_GAP_GAIN = 0.25
FAR_RADAR_LEAD_ACCEL_TAPER_FULL_GAP_EXCESS = 25.0
FAR_RADAR_LEAD_ACCEL_TAPER_FULL_GAP_GAIN = 0.9
RADARLESS_MATCHED_FOLLOW_CRUISE_HYSTERESIS_MIN = 2.5
RADARLESS_MATCHED_FOLLOW_CRUISE_HYSTERESIS_GAIN = 0.10
# Function to get parameter value based on current speed
def get_speed_based_param(speed_mph, param_array):
@@ -547,6 +549,25 @@ class LongitudinalMpc:
lead_xv = self.extrapolate_lead(x_lead, v_lead, a_lead, a_lead_tau, v_ego)
return lead_xv
@staticmethod
def get_radarless_matched_follow_cruise_hysteresis(lead, v_ego, t_follow):
if lead is None or not lead.status:
return 0.0
if not is_radarless_matched_follow_window(
v_ego,
lead.dRel,
lead.vLead,
t_follow,
radar=bool(getattr(lead, "radar", False)),
lead_brake=max(0.0, -float(getattr(lead, "aLeadK", 0.0))),
lead_prob=float(getattr(lead, "modelProb", 0.0)),
):
return 0.0
return max(RADARLESS_MATCHED_FOLLOW_CRUISE_HYSTERESIS_MIN,
RADARLESS_MATCHED_FOLLOW_CRUISE_HYSTERESIS_GAIN * float(v_ego))
def set_accel_limits(self, min_a, max_a):
# TODO this sets a max accel limit, but the minimum limit is only for cruise decel
# needs refactor
@@ -585,6 +606,11 @@ class LongitudinalMpc:
v_lower,
v_upper)
cruise_obstacle = np.cumsum(T_DIFFS * v_cruise_clipped) + get_safe_obstacle_distance(v_cruise_clipped, t_follow)
prev_source = self.source
if prev_source == 'lead0':
cruise_obstacle += self.get_radarless_matched_follow_cruise_hysteresis(lead_one, v_ego, t_follow)
elif prev_source == 'lead1':
cruise_obstacle += self.get_radarless_matched_follow_cruise_hysteresis(lead_two, v_ego, t_follow)
if tracking_lead and lead_one.status:
desired_gap = desired_follow_distance(v_ego, lead_one.vLead, t_follow)
closing_speed = max(0.0, v_ego - lead_one.vLead)
+180 -28
View File
@@ -13,6 +13,7 @@ from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import Longi
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import desired_follow_distance
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import STOP_DISTANCE
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import T_IDXS as T_IDXS_MPC
from openpilot.selfdrive.controls.lib.lead_behavior import is_radarless_matched_follow_window
from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N
from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET
from openpilot.common.swaglog import cloudlog
@@ -133,8 +134,26 @@ STEADY_FOLLOW_SMOOTHING_MIN_MODEL_PROB = 0.7
STEADY_FOLLOW_SMOOTHING_FILTER_FACTOR_FLOOR = 0.24
STEADY_FOLLOW_BRAKE_CAP_MIN_HEADWAY = 1.05
STEADY_FOLLOW_BRAKE_CAP_MAX_HEADWAY_ABOVE_TARGET = 0.90
STEADY_FOLLOW_BRAKE_CAP_MIN_REL_SPEED = -1.2
STEADY_FOLLOW_BRAKE_CAP_MAX_CLOSING_SPEED = 2.2
STEADY_FOLLOW_BRAKE_CAP_ZERO_REL_SPEED_DECEL = 0.12
STEADY_FOLLOW_BRAKE_CAP_OPENING_DECEL = 0.08
STEADY_FOLLOW_BRAKE_CAP_MIN_DECEL = 0.18
STEADY_FOLLOW_BRAKE_CAP_MAX_DECEL = 0.32
STEADY_FOLLOW_BRAKE_CAP_SPACIOUS_HEADWAY_MARGIN = 0.45
STEADY_FOLLOW_BRAKE_CAP_SPACIOUS_MIN_HEADWAY = 1.80
STEADY_FOLLOW_BRAKE_CAP_SPACIOUS_MAX_LEAD_BRAKE = 0.15
FAR_LEAD_COMFORT_BRAKE_CAP_MIN_DISTANCE = 80.0
FAR_LEAD_COMFORT_BRAKE_CAP_MIN_CLOSING_SPEED = 0.1
FAR_LEAD_COMFORT_BRAKE_CAP_MAX_CLOSING_SPEED = 2.0
FAR_LEAD_COMFORT_BRAKE_CAP_MIN_MODEL_PROB = 0.95
FAR_LEAD_COMFORT_BRAKE_CAP_MAX_LEAD_BRAKE = 0.12
FAR_LEAD_COMFORT_BRAKE_CAP_MIN_TTC = 7.5
FAR_LEAD_COMFORT_BRAKE_CAP_MIN_HEADWAY_MARGIN = 0.55
FAR_LEAD_COMFORT_BRAKE_CAP_FULL_HEADWAY_MARGIN = 1.00
FAR_LEAD_COMFORT_BRAKE_CAP_MIN_DECEL = 0.05
FAR_LEAD_COMFORT_BRAKE_CAP_MAX_DECEL = 0.18
FAR_LEAD_COMFORT_BRAKE_CAP_FULL_RELAX_DECEL = 0.05
# Lookup table for turns
_A_TOTAL_MAX_V = [3.5, 3.5, 3.2]
@@ -656,37 +675,149 @@ class LongitudinalPlanner:
gap_factor = float(np.clip(max(gap_error, 0.0) / max(gap_buffer, 0.1), 0.0, 1.0))
return float(np.interp(gap_factor, [0.0, 1.0], [near_cap, edge_cap]))
def get_matched_follow_brake_cap(self, lead, v_ego, base_t_follow):
def lead_is_matched_follow_window(self, lead, v_ego, base_t_follow):
if lead is None or not lead.status or v_ego < STEADY_FOLLOW_SMOOTHING_MIN_SPEED:
return None
return False
closing_speed = max(0.0, float(v_ego) - float(lead.vLead))
if not (STEADY_FOLLOW_SMOOTHING_MIN_CLOSING_SPEED <= closing_speed <= STEADY_FOLLOW_SMOOTHING_MAX_CLOSING_SPEED):
return None
relative_speed = float(v_ego) - float(lead.vLead)
if not (STEADY_FOLLOW_BRAKE_CAP_MIN_REL_SPEED <= relative_speed <= STEADY_FOLLOW_SMOOTHING_MAX_CLOSING_SPEED):
return False
lead_brake = max(0.0, -float(getattr(lead, "aLeadK", 0.0)))
if lead_brake > STEADY_FOLLOW_SMOOTHING_MAX_LEAD_BRAKE:
return None
return False
lead_prob = float(getattr(lead, "modelProb", 1.0 if bool(getattr(lead, "radar", False)) else 0.0))
if not bool(getattr(lead, "radar", False)) and lead_prob < STEADY_FOLLOW_SMOOTHING_MIN_MODEL_PROB:
return None
lead_radar = bool(getattr(lead, "radar", False))
lead_prob = float(getattr(lead, "modelProb", 1.0 if lead_radar else 0.0))
if not lead_radar and not is_radarless_matched_follow_window(
v_ego,
lead.dRel,
lead.vLead,
base_t_follow,
radar=lead_radar,
lead_brake=lead_brake,
lead_prob=lead_prob,
):
return False
actual_headway = float(lead.dRel) / max(float(v_ego), 1e-3)
if actual_headway < max(STEADY_FOLLOW_BRAKE_CAP_MIN_HEADWAY, float(base_t_follow) - STEADY_FOLLOW_SMOOTHING_HEADWAY_BELOW_TARGET):
return None
return False
if actual_headway > float(base_t_follow) + STEADY_FOLLOW_BRAKE_CAP_MAX_HEADWAY_ABOVE_TARGET:
return False
return True
def get_matched_follow_control_lead(self, v_ego, t_follow):
if self.mpc.source == 'lead1' and self.lead_is_matched_follow_window(self.lead_two, v_ego, t_follow):
return self.lead_two
if self.lead_is_matched_follow_window(self.lead_one, v_ego, t_follow):
return self.lead_one
if self.lead_is_matched_follow_window(self.lead_two, v_ego, t_follow):
return self.lead_two
return None
def get_follow_control_lead(self, lead_control_active, v_ego, t_follow):
matched_follow_lead = self.get_matched_follow_control_lead(v_ego, t_follow)
if matched_follow_lead is not None:
return matched_follow_lead
if not lead_control_active:
return None
if self.mpc.source == 'lead1' and self.lead_is_matched_follow_window(self.lead_two, v_ego, t_follow):
return self.lead_two
if self.lead_one.status:
return self.lead_one
if self.lead_two.status:
return self.lead_two
return None
def lead_is_spacious_brake_cap_window(self, lead, v_ego, base_t_follow):
if lead is None or not lead.status or v_ego < STEADY_FOLLOW_SMOOTHING_MIN_SPEED:
return False
relative_speed = float(v_ego) - float(lead.vLead)
if not (STEADY_FOLLOW_BRAKE_CAP_MIN_REL_SPEED <= relative_speed <= STEADY_FOLLOW_BRAKE_CAP_MAX_CLOSING_SPEED):
return False
lead_brake = max(0.0, -float(getattr(lead, "aLeadK", 0.0)))
if lead_brake > STEADY_FOLLOW_BRAKE_CAP_SPACIOUS_MAX_LEAD_BRAKE:
return False
lead_radar = bool(getattr(lead, "radar", False))
lead_prob = float(getattr(lead, "modelProb", 1.0 if lead_radar else 0.0))
if not lead_radar and lead_prob < STEADY_FOLLOW_SMOOTHING_MIN_MODEL_PROB:
return False
actual_headway = float(lead.dRel) / max(float(v_ego), 1e-3)
if actual_headway < max(float(base_t_follow) + STEADY_FOLLOW_BRAKE_CAP_SPACIOUS_HEADWAY_MARGIN,
STEADY_FOLLOW_BRAKE_CAP_SPACIOUS_MIN_HEADWAY):
return False
if actual_headway > float(base_t_follow) + STEADY_FOLLOW_BRAKE_CAP_MAX_HEADWAY_ABOVE_TARGET:
return False
return True
def get_matched_follow_brake_cap(self, lead, v_ego, base_t_follow):
if not (
self.lead_is_matched_follow_window(lead, v_ego, base_t_follow) or
self.lead_is_spacious_brake_cap_window(lead, v_ego, base_t_follow)
):
return None
relative_speed = float(v_ego) - float(lead.vLead)
actual_headway = float(lead.dRel) / max(float(v_ego), 1e-3)
cap_decel = float(np.interp(
closing_speed,
[STEADY_FOLLOW_SMOOTHING_MIN_CLOSING_SPEED, STEADY_FOLLOW_SMOOTHING_MAX_CLOSING_SPEED],
[STEADY_FOLLOW_BRAKE_CAP_MIN_DECEL, STEADY_FOLLOW_BRAKE_CAP_MAX_DECEL],
relative_speed,
[STEADY_FOLLOW_BRAKE_CAP_MIN_REL_SPEED, 0.0, STEADY_FOLLOW_BRAKE_CAP_MAX_CLOSING_SPEED],
[STEADY_FOLLOW_BRAKE_CAP_OPENING_DECEL,
STEADY_FOLLOW_BRAKE_CAP_ZERO_REL_SPEED_DECEL,
STEADY_FOLLOW_BRAKE_CAP_MAX_DECEL],
))
headway_deficit = float(np.clip((float(base_t_follow) - actual_headway) / STEADY_FOLLOW_SMOOTHING_HEADWAY_BELOW_TARGET, 0.0, 1.0))
cap_decel = min(STEADY_FOLLOW_BRAKE_CAP_MAX_DECEL, cap_decel + 0.05 * headway_deficit)
return -cap_decel
def get_far_lead_brake_cap(self, lead, v_ego, base_t_follow):
if lead is None or not lead.status or v_ego < STEADY_FOLLOW_SMOOTHING_MIN_SPEED:
return None
if bool(getattr(lead, "radar", False)):
return None
lead_prob = float(getattr(lead, "modelProb", 0.0))
if lead_prob < FAR_LEAD_COMFORT_BRAKE_CAP_MIN_MODEL_PROB:
return None
relative_speed = float(v_ego) - float(lead.vLead)
if not (FAR_LEAD_COMFORT_BRAKE_CAP_MIN_CLOSING_SPEED <= relative_speed <= FAR_LEAD_COMFORT_BRAKE_CAP_MAX_CLOSING_SPEED):
return None
lead_brake = max(0.0, -float(getattr(lead, "aLeadK", 0.0)))
if lead_brake > FAR_LEAD_COMFORT_BRAKE_CAP_MAX_LEAD_BRAKE:
return None
actual_headway = float(lead.dRel) / max(float(v_ego), 1e-3)
headway_margin = actual_headway - float(base_t_follow)
if float(lead.dRel) < FAR_LEAD_COMFORT_BRAKE_CAP_MIN_DISTANCE or headway_margin < FAR_LEAD_COMFORT_BRAKE_CAP_MIN_HEADWAY_MARGIN:
return None
ttc = float(lead.dRel) / max(relative_speed, 1e-3)
if ttc < FAR_LEAD_COMFORT_BRAKE_CAP_MIN_TTC:
return None
cap_decel = float(np.interp(
relative_speed,
[FAR_LEAD_COMFORT_BRAKE_CAP_MIN_CLOSING_SPEED, FAR_LEAD_COMFORT_BRAKE_CAP_MAX_CLOSING_SPEED],
[FAR_LEAD_COMFORT_BRAKE_CAP_MIN_DECEL, FAR_LEAD_COMFORT_BRAKE_CAP_MAX_DECEL],
))
relax_decel = float(np.interp(
headway_margin,
[FAR_LEAD_COMFORT_BRAKE_CAP_MIN_HEADWAY_MARGIN, FAR_LEAD_COMFORT_BRAKE_CAP_FULL_HEADWAY_MARGIN],
[0.0, FAR_LEAD_COMFORT_BRAKE_CAP_FULL_RELAX_DECEL],
))
return -max(0.0, cap_decel - relax_decel)
@staticmethod
def raw_close_lead_needs_control(lead, v_ego):
if lead is None or not lead.status:
@@ -720,6 +851,7 @@ class LongitudinalPlanner:
accel_coast = ACCEL_MAX
v_ego = get_planner_v_ego(self.CP, sm['carState'])
scene_v_ego = float(sm['carState'].vEgo)
v_cruise = sm['starpilotPlan'].vCruise
if not np.isfinite(v_cruise):
cloudlog.error(f"Longitudinal planner received non-finite vCruise={v_cruise}, falling back to v_ego={v_ego:.2f}")
@@ -784,7 +916,7 @@ class LongitudinalPlanner:
tracking_lead = bool(sm['starpilotPlan'].trackingLead)
self.lead_one = sm['radarState'].leadOne
self.lead_two = sm['radarState'].leadTwo
raw_close_lead_control = any(self.raw_close_lead_needs_control(lead, v_ego) for lead in (self.lead_one, self.lead_two))
raw_close_lead_control = any(self.raw_close_lead_needs_control(lead, scene_v_ego) for lead in (self.lead_one, self.lead_two))
# StarPilot trackingLead is debounce/model-length based. Keep a raw close-lead
# safety path so ACC/chill does not ignore a visible lead during that debounce.
lead_control_active = tracking_lead or raw_close_lead_control
@@ -897,13 +1029,14 @@ class LongitudinalPlanner:
closing_speed = 0.0
if lead_one_active:
desired_gap = float(desired_follow_distance(v_ego, self.lead_one.vLead, effective_t_follow))
scene_desired_gap = float(desired_follow_distance(scene_v_ego, self.lead_one.vLead, effective_t_follow))
close_gap_window = max(UNCERT_PANIC_MAX_GAP_BUFFER_MIN,
UNCERT_PANIC_MAX_GAP_BUFFER_GAIN * float(v_ego))
panic_close_window = float(self.lead_one.dRel) <= desired_gap + close_gap_window
closing_speed = max(0.0, v_ego - self.lead_one.vLead)
panic_close_window = float(self.lead_one.dRel) <= scene_desired_gap + close_gap_window
closing_speed = max(0.0, scene_v_ego - self.lead_one.vLead)
closing_fast = closing_speed >= max(
UNCERT_PANIC_MIN_CLOSING_SPEED,
UNCERT_PANIC_MIN_CLOSING_SPEED_GAIN * float(v_ego),
UNCERT_PANIC_MIN_CLOSING_SPEED_GAIN * float(scene_v_ego),
)
# Only bypass lead smoothing when we're closing meaningfully and already
@@ -916,16 +1049,27 @@ class LongitudinalPlanner:
steady_follow_filter_floor = 0.0
if lead_one_active and desired_gap is not None and not panic_bypass:
lead_brake = max(0.0, -float(getattr(self.lead_one, "aLeadK", 0.0)))
lead_prob = float(getattr(self.lead_one, "modelProb", 1.0 if bool(getattr(self.lead_one, "radar", False)) else 0.0))
actual_headway = float(self.lead_one.dRel) / max(float(v_ego), 1e-3)
lead_radar = bool(getattr(self.lead_one, "radar", False))
lead_prob = float(getattr(self.lead_one, "modelProb", 1.0 if lead_radar else 0.0))
actual_headway = float(self.lead_one.dRel) / max(scene_v_ego, 1e-3)
matched_follow_window = (
v_ego >= STEADY_FOLLOW_SMOOTHING_MIN_SPEED and
STEADY_FOLLOW_SMOOTHING_MIN_CLOSING_SPEED <= closing_speed <= STEADY_FOLLOW_SMOOTHING_MAX_CLOSING_SPEED and
actual_headway >= max(STEADY_FOLLOW_SMOOTHING_MIN_HEADWAY,
effective_t_follow - STEADY_FOLLOW_SMOOTHING_HEADWAY_BELOW_TARGET) and
actual_headway <= effective_t_follow + STEADY_FOLLOW_SMOOTHING_HEADWAY_ABOVE_TARGET and
lead_brake <= STEADY_FOLLOW_SMOOTHING_MAX_LEAD_BRAKE and
(bool(getattr(self.lead_one, "radar", False)) or lead_prob >= STEADY_FOLLOW_SMOOTHING_MIN_MODEL_PROB)
is_radarless_matched_follow_window(
scene_v_ego,
self.lead_one.dRel,
self.lead_one.vLead,
effective_t_follow,
radar=lead_radar,
lead_brake=lead_brake,
lead_prob=lead_prob,
) or (
lead_radar and
scene_v_ego >= STEADY_FOLLOW_SMOOTHING_MIN_SPEED and
STEADY_FOLLOW_SMOOTHING_MIN_CLOSING_SPEED <= closing_speed <= STEADY_FOLLOW_SMOOTHING_MAX_CLOSING_SPEED and
actual_headway >= max(STEADY_FOLLOW_SMOOTHING_MIN_HEADWAY,
effective_t_follow - STEADY_FOLLOW_SMOOTHING_HEADWAY_BELOW_TARGET) and
actual_headway <= effective_t_follow + STEADY_FOLLOW_SMOOTHING_HEADWAY_ABOVE_TARGET and
lead_brake <= STEADY_FOLLOW_SMOOTHING_MAX_LEAD_BRAKE
)
)
if matched_follow_window:
steady_follow_filter_floor = STEADY_FOLLOW_SMOOTHING_FILTER_FACTOR_FLOOR
@@ -1134,12 +1278,20 @@ class LongitudinalPlanner:
if vision_brake_cap_active:
output_accel_min = min(output_accel_min, vision_cap_accel_min)
if lead_one_active and not panic_bypass:
matched_follow_brake_cap = self.get_matched_follow_brake_cap(self.lead_one, v_ego, sm['starpilotPlan'].tFollow)
follow_control_lead = self.get_follow_control_lead(lead_control_active, scene_v_ego, effective_t_follow)
if follow_control_lead is not None and not panic_bypass:
matched_follow_brake_cap = self.get_matched_follow_brake_cap(follow_control_lead, scene_v_ego, effective_t_follow)
if matched_follow_brake_cap is not None:
self.a_desired = max(self.a_desired, matched_follow_brake_cap)
output_a_target = max(output_a_target, matched_follow_brake_cap)
comfort_lead = self.lead_two if self.mpc.source == 'lead1' and self.lead_two.status else self.lead_one
if comfort_lead is not None and not panic_bypass:
far_lead_brake_cap = self.get_far_lead_brake_cap(comfort_lead, scene_v_ego, effective_t_follow)
if far_lead_brake_cap is not None:
self.a_desired = max(self.a_desired, far_lead_brake_cap)
output_a_target = max(output_a_target, far_lead_brake_cap)
output_accel_max = no_throttle_output_max if not self.allow_throttle else accel_limits_turns[1]
output_a_target = float(np.clip(output_a_target, output_accel_min, output_accel_max))
@@ -1,5 +1,6 @@
from openpilot.selfdrive.controls.lib.lead_behavior import (
get_tracked_lead_catchup_bias,
is_radarless_matched_follow_window,
should_track_lead,
should_disable_far_lead_throttle,
)
@@ -69,3 +70,19 @@ def test_should_track_lead_accepts_closer_vision_only_highway_lead():
def test_should_track_lead_accepts_fast_closing_vision_lead_early():
assert should_track_lead(True, 90.0, 140.0, 6.0, 20.0, v_lead=0.0, radar=False)
def test_radarless_matched_follow_window_accepts_pace_matched_highway_follow():
assert is_radarless_matched_follow_window(31.0, 48.0, 30.4, 1.45, radar=False, lead_brake=0.05, lead_prob=0.95)
def test_radarless_matched_follow_window_rejects_large_relative_speed():
assert not is_radarless_matched_follow_window(31.0, 48.0, 27.5, 1.45, radar=False, lead_brake=0.05, lead_prob=0.95)
def test_radarless_matched_follow_window_rejects_far_headway():
assert not is_radarless_matched_follow_window(31.0, 82.0, 30.4, 1.45, radar=False, lead_brake=0.05, lead_prob=0.95)
def test_radarless_matched_follow_window_rejects_low_confidence_lead():
assert not is_radarless_matched_follow_window(31.0, 48.0, 30.4, 1.45, radar=False, lead_brake=0.05, lead_prob=0.55)
@@ -995,3 +995,100 @@ def test_near_speed_follow_soft_brake_cap_limits_matched_follow_pulse():
assert planner.mpc.filter_time_factor >= 0.24
assert planner.output_a_target >= -0.33
def test_near_speed_follow_soft_brake_cap_covers_slightly_opening_lead():
v_ego = 29.58
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
planner = LongitudinalPlanner(CP, init_v=v_ego)
lead = make_lead(status=True, d_rel=44.44, v_lead=30.65, radar=False, model_prob=0.98)
cap = planner.get_matched_follow_brake_cap(lead, v_ego, 1.45)
assert cap is not None
assert cap >= -0.14
assert cap <= -0.06
def test_near_speed_follow_soft_brake_cap_extends_to_spacious_modest_closing():
v_ego = 23.69
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
planner = LongitudinalPlanner(CP, init_v=v_ego)
lead = make_lead(status=True, d_rel=49.48, v_lead=21.64, a_lead=-0.014, radar=False, model_prob=0.998)
cap = planner.get_matched_follow_brake_cap(lead, v_ego, 1.45)
assert cap is not None
assert cap >= -0.33
assert cap <= -0.22
def test_near_speed_follow_soft_brake_cap_uses_raw_vehicle_speed_when_cluster_runs_high():
v_ego = 23.0073
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
planner = LongitudinalPlanner(CP, init_v=v_ego)
sm = make_sm(
v_ego,
desired_accel=0.0,
min_accel=-0.5,
experimental_mode=False,
tracking_lead=True,
lead_one=make_lead(status=True, d_rel=47.52, v_lead=21.68, a_lead=-0.0646, radar=False, model_prob=0.998),
)
sm["carState"].vEgoCluster = 23.6931
sm["starpilotPlan"].maxAcceleration = 0.61
for _ in range(6):
planner.update(sm, make_toggles())
assert not planner.lead_is_matched_follow_window(sm["radarState"].leadOne, sm["carState"].vEgoCluster, 1.45)
assert planner.output_a_target > -0.35
assert planner.output_a_target < -0.22
def test_near_speed_follow_soft_brake_cap_rejects_close_gap_even_with_modest_closing():
v_ego = 37.19
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
planner = LongitudinalPlanner(CP, init_v=v_ego)
lead = make_lead(status=True, d_rel=34.70, v_lead=35.88, radar=False, model_prob=0.99)
cap = planner.get_matched_follow_brake_cap(lead, v_ego, 1.0)
assert cap is None
def test_follow_control_lead_prefers_active_lead1_for_matched_follow():
v_ego = 23.3
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
planner = LongitudinalPlanner(CP, init_v=v_ego)
planner.lead_one = make_lead(status=True, d_rel=80.0, v_lead=20.0, radar=False, model_prob=0.6)
planner.lead_two = make_lead(status=True, d_rel=49.9, v_lead=21.9, radar=False, model_prob=0.98)
planner.mpc.source = "lead1"
follow_lead = planner.get_follow_control_lead(True, v_ego, 1.45)
assert follow_lead is planner.lead_two
def test_follow_control_lead_keeps_matched_follow_lead_without_tracking_latch():
v_ego = 27.5
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
planner = LongitudinalPlanner(CP, init_v=v_ego)
planner.lead_one = make_lead(status=True, d_rel=61.99, v_lead=27.63, radar=False, model_prob=0.99)
follow_lead = planner.get_follow_control_lead(False, v_ego, 1.45)
assert follow_lead is planner.lead_one
def test_far_lead_soft_brake_cap_limits_high_confidence_distant_vision_lead():
v_ego = 32.37
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
planner = LongitudinalPlanner(CP, init_v=v_ego)
lead = make_lead(status=True, d_rel=82.07, v_lead=30.63, a_lead=-0.01, radar=False, model_prob=0.99)
cap = planner.get_far_lead_brake_cap(lead, v_ego, 1.70)
assert cap is not None
assert cap > -0.2
assert cap < -0.05
@@ -41,6 +41,7 @@ from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import (
draw_soft_card,
draw_tab_card,
)
from openpilot.starpilot.common.connect_server import prepare_konik_server_switch
LEGACY_STARPILOT_PARAM_RENAMES = {
"FrogPilotApiToken": "StarPilotApiToken",
@@ -981,7 +982,7 @@ class StarPilotSystemLayout(_SettingsPage):
return self._params.get_bool("UseKonikServer")
def _on_konik_toggle(self, state):
self._params.put_bool("UseKonikServer", state)
prepare_konik_server_switch(state, self._params)
cache_path = Path("/cache/use_konik")
if state:
cache_path.parent.mkdir(parents=True, exist_ok=True)
+2 -1
View File
@@ -23,6 +23,7 @@ from openpilot.selfdrive.ui.ui_state import device, ui_state
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets.html_render import HtmlModal, HtmlRenderer
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
from openpilot.starpilot.common.connect_server import prepare_konik_server_switch
class ReviewTermsPage(TermsPage, NavScroller):
@@ -152,7 +153,7 @@ class ConnectServerBigButton(BigButton):
def _apply_selection(self, selection: str):
use_konik = selection == self._KONIK_OPTION
self._params.put_bool("UseKonikServer", use_konik)
prepare_konik_server_switch(use_konik, self._params)
toggle_path = _konik_toggle_path()
toggle_path.parent.mkdir(parents=True, exist_ok=True)
+79
View File
@@ -0,0 +1,79 @@
from pathlib import Path
from openpilot.common.params import Params
from openpilot.system.athena.registration import register
from openpilot.system.hardware import PC
from openpilot.system.hardware.hw import Paths
from openpilot.starpilot.common.starpilot_utilities import use_konik_server
def _cache_params_path() -> str:
if PC:
return str(Path(Paths.comma_home()) / "cache" / "params")
return "/cache/params"
def _normalize_dongle_id(value):
if isinstance(value, bytes):
value = value.decode("utf-8", errors="ignore")
if value is None:
return None
value = str(value).strip()
return value or None
def _read_persisted_stock_dongle_id():
persisted_dongle_id_path = Path(Paths.persist_root()) / "comma" / "dongle_id"
if not persisted_dongle_id_path.is_file():
return None
return _normalize_dongle_id(persisted_dongle_id_path.read_text())
def _remove_param_from_live_and_cache(key, params, params_cache=None):
if params_cache is None:
params_cache = Params(_cache_params_path())
params.remove(key)
params_cache.remove(key)
def prepare_konik_server_switch(use_konik, params, params_cache=None):
params.put_bool("UseKonikServer", use_konik)
if use_konik:
_remove_param_from_live_and_cache("KonikDongleId", params, params_cache)
else:
_remove_param_from_live_and_cache("DongleId", params, params_cache)
def _ensure_stock_dongle_id(params):
current_dongle_id = _normalize_dongle_id(params.get("DongleId"))
konik_dongle_id = _normalize_dongle_id(params.get("KonikDongleId"))
stock_dongle_id = _normalize_dongle_id(params.get("StockDongleId"))
if stock_dongle_id not in (None, konik_dongle_id):
return stock_dongle_id
candidate = _read_persisted_stock_dongle_id()
if candidate in (None, konik_dongle_id):
candidate = current_dongle_id if current_dongle_id != konik_dongle_id else None
if candidate is not None and candidate != stock_dongle_id:
params.put("StockDongleId", candidate)
return candidate
def sync_konik_dongle_id(params):
current_dongle_id = _normalize_dongle_id(params.get("DongleId"))
konik_dongle_id = _normalize_dongle_id(params.get("KonikDongleId"))
stock_dongle_id = _ensure_stock_dongle_id(params)
if use_konik_server():
if konik_dongle_id is None:
konik_dongle_id = _normalize_dongle_id(register(show_spinner=True, register_konik=True))
if konik_dongle_id is not None:
params.put("KonikDongleId", konik_dongle_id)
if konik_dongle_id is not None and current_dongle_id != konik_dongle_id:
params.put("DongleId", konik_dongle_id)
elif current_dongle_id == konik_dongle_id and stock_dongle_id is not None:
params.put("DongleId", stock_dongle_id)
+2 -53
View File
@@ -12,72 +12,21 @@ from cereal import messaging
from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params
from openpilot.common.time_helpers import system_time_valid
from openpilot.system.athena.registration import register
from openpilot.system.hardware import HARDWARE
from openpilot.system.hardware.hw import Paths
from openpilot.system.version import get_build_metadata
from openpilot.starpilot.assets.theme_manager import ThemeManager
from openpilot.starpilot.common.starpilot_backups import backup_starpilot
from openpilot.starpilot.common.connect_server import sync_konik_dongle_id
from openpilot.starpilot.common.maps_catalog import normalize_schedule_value, sanitize_selected_locations_csv
from openpilot.starpilot.common.theme_asset_names import find_matching_theme_asset_file
from openpilot.starpilot.common.starpilot_utilities import get_starpilot_api_info, is_FrogsGoMoo, is_url_pingable, run_cmd, use_konik_server
from openpilot.starpilot.common.starpilot_utilities import get_starpilot_api_info, is_FrogsGoMoo, is_url_pingable, run_cmd
from openpilot.starpilot.common.starpilot_variables import (
ERROR_LOGS_PATH, STARPILOT_API, FROGS_GO_MOO_PATH, HD_LOGS_PATH, KONIK_LOGS_PATH, MAPS_PATH, THEME_SAVE_PATH,
StarPilotVariables, get_starpilot_toggles
)
def _normalize_dongle_id(value):
if isinstance(value, bytes):
value = value.decode("utf-8", errors="ignore")
if value is None:
return None
value = str(value).strip()
return value or None
def _read_persisted_stock_dongle_id():
persisted_dongle_id_path = Path(Paths.persist_root()) / "comma" / "dongle_id"
if not persisted_dongle_id_path.is_file():
return None
return _normalize_dongle_id(persisted_dongle_id_path.read_text())
def _ensure_stock_dongle_id(params):
current_dongle_id = _normalize_dongle_id(params.get("DongleId"))
konik_dongle_id = _normalize_dongle_id(params.get("KonikDongleId"))
stock_dongle_id = _normalize_dongle_id(params.get("StockDongleId"))
if stock_dongle_id not in (None, konik_dongle_id):
return stock_dongle_id
candidate = _read_persisted_stock_dongle_id()
if candidate in (None, konik_dongle_id):
candidate = current_dongle_id if current_dongle_id != konik_dongle_id else None
if candidate is not None and candidate != stock_dongle_id:
params.put("StockDongleId", candidate)
return candidate
def sync_konik_dongle_id(params):
current_dongle_id = _normalize_dongle_id(params.get("DongleId"))
konik_dongle_id = _normalize_dongle_id(params.get("KonikDongleId"))
stock_dongle_id = _ensure_stock_dongle_id(params)
if use_konik_server():
if konik_dongle_id is None:
konik_dongle_id = _normalize_dongle_id(register(show_spinner=True, register_konik=True))
if konik_dongle_id is not None:
params.put("KonikDongleId", konik_dongle_id)
if konik_dongle_id is not None and current_dongle_id != konik_dongle_id:
params.put("DongleId", konik_dongle_id)
elif current_dongle_id == konik_dongle_id and stock_dongle_id is not None:
params.put("DongleId", stock_dongle_id)
def seed_desktop_theme_assets():
params = Params()
params_memory = Params(memory=True)
+7 -1
View File
@@ -457,7 +457,13 @@ class StarPilotVariables:
current_value = self.params.get_float(key)
if not math.isfinite(current_value):
current_value = 0.0
if math.isclose(current_value, current_stock, abs_tol=1e-6) or math.isclose(current_stock, 0.0, abs_tol=1e-6):
# If the stock baseline was missing (0.0/unset), do not stomp an existing
# user override. Only backfill the live param when it was still effectively
# tracking the old stock value or was itself unset.
should_update_live_value = math.isclose(current_value, current_stock, abs_tol=1e-6)
should_update_live_value |= math.isclose(current_value, 0.0, abs_tol=1e-6)
if should_update_live_value:
self.params.put_float(key, live_value)
self.params.put_float(stock_key, live_value)
@@ -1,4 +1,4 @@
from openpilot.starpilot.common import starpilot_functions as spf
from openpilot.starpilot.common import connect_server as cs
class FakeParams:
@@ -11,15 +11,21 @@ class FakeParams:
def put(self, key, value):
self.values[key] = value
def put_bool(self, key, value):
self.values[key] = b"1" if value else b"0"
def remove(self, key):
self.values.pop(key, None)
def test_sync_konik_dongle_id_preserves_stock_id_before_switching(monkeypatch, tmp_path):
monkeypatch.setattr(spf.Paths, "persist_root", staticmethod(lambda: str(tmp_path)))
monkeypatch.setattr(spf, "use_konik_server", lambda: True)
monkeypatch.setattr(spf, "register", lambda **kwargs: "konik-dongle")
monkeypatch.setattr(cs.Paths, "persist_root", staticmethod(lambda: str(tmp_path)))
monkeypatch.setattr(cs, "use_konik_server", lambda: True)
monkeypatch.setattr(cs, "register", lambda **kwargs: "konik-dongle")
params = FakeParams({"DongleId": "stock-dongle"})
spf.sync_konik_dongle_id(params)
cs.sync_konik_dongle_id(params)
assert params.get("StockDongleId") == "stock-dongle"
assert params.get("KonikDongleId") == "konik-dongle"
@@ -32,30 +38,52 @@ def test_sync_konik_dongle_id_restores_stock_id_from_persist(monkeypatch, tmp_pa
persisted_dongle_id_path.parent.mkdir(parents=True, exist_ok=True)
persisted_dongle_id_path.write_text("stock-dongle")
monkeypatch.setattr(spf.Paths, "persist_root", staticmethod(lambda: str(persist_root)))
monkeypatch.setattr(spf, "use_konik_server", lambda: False)
monkeypatch.setattr(cs.Paths, "persist_root", staticmethod(lambda: str(persist_root)))
monkeypatch.setattr(cs, "use_konik_server", lambda: False)
params = FakeParams({
"DongleId": "konik-dongle",
"KonikDongleId": "konik-dongle",
})
spf.sync_konik_dongle_id(params)
cs.sync_konik_dongle_id(params)
assert params.get("StockDongleId") == "stock-dongle"
assert params.get("DongleId") == "stock-dongle"
def test_sync_konik_dongle_id_skips_missing_stock_backup(monkeypatch, tmp_path):
monkeypatch.setattr(spf.Paths, "persist_root", staticmethod(lambda: str(tmp_path)))
monkeypatch.setattr(spf, "use_konik_server", lambda: False)
monkeypatch.setattr(cs.Paths, "persist_root", staticmethod(lambda: str(tmp_path)))
monkeypatch.setattr(cs, "use_konik_server", lambda: False)
params = FakeParams({
"DongleId": "konik-dongle",
"KonikDongleId": "konik-dongle",
})
spf.sync_konik_dongle_id(params)
cs.sync_konik_dongle_id(params)
assert params.get("DongleId") == "konik-dongle"
assert params.get("StockDongleId") is None
def test_prepare_konik_server_switch_clears_cached_konik_id():
params = FakeParams({"KonikDongleId": "konik-dongle"})
params_cache = FakeParams({"KonikDongleId": "konik-dongle"})
cs.prepare_konik_server_switch(True, params, params_cache)
assert params.get("UseKonikServer") == b"1"
assert params.get("KonikDongleId") is None
assert params_cache.get("KonikDongleId") is None
def test_prepare_konik_server_switch_clears_cached_stock_id():
params = FakeParams({"DongleId": "konik-dongle"})
params_cache = FakeParams({"DongleId": "konik-dongle"})
cs.prepare_konik_server_switch(False, params, params_cache)
assert params.get("UseKonikServer") == b"0"
assert params.get("DongleId") is None
assert params_cache.get("DongleId") is None
@@ -27,3 +27,28 @@ def test_get_starpilot_toggles_uses_last_non_empty_broadcast(monkeypatch):
assert first.always_on_lateral is True
assert second.always_on_lateral is True
assert second.vision_speed_limit_detection is True
class _FakeParams:
def __init__(self, floats=None):
self.floats = dict(floats or {})
def get_float(self, key):
return float(self.floats.get(key, 0.0))
def put_float(self, key, value):
self.floats[key] = float(value)
def remove(self, key):
self.floats.pop(key, None)
def test_sync_stock_param_does_not_stomp_existing_custom_value_when_stock_missing():
params = _FakeParams({"SteerDelay": 0.35, "SteerDelayStock": 0.0})
variables = object.__new__(spv.StarPilotVariables)
variables.params = params
variables._sync_stock_param("SteerDelay", "SteerDelayStock", 0.10)
assert params.get_float("SteerDelay") == 0.35
assert params.get_float("SteerDelayStock") == 0.10
+26 -2
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
import json
import math
import time
import cereal.messaging as messaging
@@ -10,7 +11,7 @@ from openpilot.common.gps import get_gps_location_service
from openpilot.common.params import Params
from openpilot.common.realtime import DT_MDL
from openpilot.selfdrive.car.cruise import V_CRUISE_MAX, V_CRUISE_UNSET
from openpilot.selfdrive.controls.lib.lead_behavior import should_track_lead
from openpilot.selfdrive.controls.lib.lead_behavior import is_radarless_matched_follow_window, should_track_lead
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import A_CHANGE_COST, DANGER_ZONE_COST, J_EGO_COST, STOP_DISTANCE
from openpilot.starpilot.common.starpilot_utilities import calculate_lane_width, calculate_road_curvature
@@ -22,6 +23,8 @@ from openpilot.starpilot.controls.lib.starpilot_following import StarPilotFollow
from openpilot.starpilot.controls.lib.starpilot_vcruise import StarPilotVCruise
from openpilot.starpilot.controls.lib.weather_checker import WeatherChecker
RADARLESS_TRACK_HOLD_TIME = 0.45
def _sanitize_json_value(value):
if isinstance(value, float):
@@ -71,6 +74,7 @@ class StarPilotPlanner:
self.gps_location_service = get_gps_location_service(self.params)
self.tracking_lead_filter = FirstOrderFilter(0, 0.5, DT_MDL)
self.radarless_follow_hold_until = 0.0
def shutdown(self):
self.starpilot_vcruise.slc.shutdown()
@@ -101,7 +105,7 @@ class StarPilotPlanner:
}
self.gps_valid = self.gps_position["latitude"] != 0 or self.gps_position["longitude"] != 0
bearing = self.gps_position["bearing"]
if starpilot_toggles.compass and abs(bearing - self._prev_gps_bearing) > 0.5:
if getattr(starpilot_toggles, "compass", False) and abs(bearing - self._prev_gps_bearing) > 0.5:
self.params_memory.put("LastGPSPosition", json.dumps(self.gps_position))
self._prev_gps_bearing = bearing
@@ -164,6 +168,26 @@ class StarPilotPlanner:
v_lead=self.lead_one.vLead,
radar=bool(getattr(self.lead_one, "radar", False)),
)
now_t = time.monotonic()
lead_radar = bool(getattr(self.lead_one, "radar", False))
t_follow = max(float(getattr(self.starpilot_following, "t_follow", 0.0)), 1.45)
matched_follow_window = self.lead_one.status and is_radarless_matched_follow_window(
v_ego,
self.lead_one.dRel,
self.lead_one.vLead,
t_follow,
radar=lead_radar,
lead_brake=max(0.0, -float(getattr(self.lead_one, "aLeadK", 0.0))),
lead_prob=float(getattr(self.lead_one, "modelProb", 0.0)),
)
if matched_follow_window and (following_lead or self.tracking_lead or self.tracking_lead_filter.x >= THRESHOLD * 0.6):
self.radarless_follow_hold_until = now_t + RADARLESS_TRACK_HOLD_TIME
elif lead_radar or not self.lead_one.status:
self.radarless_follow_hold_until = 0.0
if not following_lead and matched_follow_window and now_t < self.radarless_follow_hold_until:
following_lead = True
self.tracking_lead_filter.update(following_lead)
return self.tracking_lead_filter.x >= THRESHOLD
@@ -1,6 +1,27 @@
#include "starpilot/ui/screenrecorder/screenrecorder.h"
#include "starpilot/ui/qt/offroad/device_settings.h"
namespace {
std::string cacheParamsPath() {
return Hardware::PC() ? Path::comma_home() + "/cache/params" : "/cache/params";
}
void prepareKonikServerSwitch(bool use_konik) {
Params params;
Params params_cache(cacheParamsPath());
if (use_konik) {
params.remove("KonikDongleId");
params_cache.remove("KonikDongleId");
} else {
params.remove("DongleId");
params_cache.remove("DongleId");
}
}
} // namespace
StarPilotDevicePanel::StarPilotDevicePanel(StarPilotSettingsWindow *parent, bool forceOpen) : StarPilotListWidget(parent), parent(parent) {
forceOpenDescriptions = forceOpen;
@@ -165,6 +186,7 @@ StarPilotDevicePanel::StarPilotDevicePanel(StarPilotSettingsWindow *parent, bool
filePath = "/cache/use_HD";
} else if (key == "UseKonikServer") {
filePath = "/cache/use_konik";
prepareKonikServerSwitch(state);
}
if (!filePath.isEmpty()) {
+130 -4
View File
@@ -5,6 +5,7 @@ import math
import os
from collections import defaultdict
from dataclasses import asdict, dataclass
from pathlib import Path
from types import SimpleNamespace
import numpy as np
@@ -17,6 +18,7 @@ from openpilot.selfdrive.controls.lib import longitudinal_planner as longitudina
from openpilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanner
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import STOP_DISTANCE
from openpilot.tools.lib.logreader import LogReader
from openpilot.tools.lib.url_file import hash_256
for level_name in ("info", "warning", "error", "exception", "event"):
@@ -43,6 +45,10 @@ REQUIRED_SERVICES = {
"selfdriveState",
"starpilotPlan",
}
COMMA_DATA_CACHE_ROOT = Path("/tmp/comma_download_cache")
ROUTE_CACHE_REBUILD_ROOT = Path("/tmp/starpilot_route_cache")
MAX_CACHED_ROUTE_SEGMENTS = 255
MAX_CONSECUTIVE_CACHE_MISSES = 4
@dataclass
@@ -141,8 +147,117 @@ def is_vision_slow_lead(v_ego: float, lead_status: bool, lead_radar: bool, lead_
)
def score_route(route: str, capture_events: bool = False, max_events: int = 200) -> tuple[RouteMetrics, list[dict]]:
metrics = RouteMetrics(route=route)
def cached_rlog_url(route: str, seg: int) -> str:
return f"https://commadata2.blob.core.windows.net/commadata2/{route}/{seg}/rlog.zst"
def parse_route_identifier(identifier: str) -> tuple[str, str | None]:
parts = identifier.split("/")
if len(parts) < 2:
raise ValueError(f"Invalid route identifier: {identifier}")
route = "/".join(parts[:2])
remainder = parts[2:]
if not remainder:
return route, None
first = remainder[0]
if first in ("r", "q", "a", "i"):
return route, None
return route, first
def cached_rlog_chunk_paths(route: str, seg: int, cache_root: Path = COMMA_DATA_CACHE_ROOT) -> list[Path]:
prefix = f"{hash_256(cached_rlog_url(route, seg))}_"
chunk_paths = []
for candidate in cache_root.glob(f"{prefix}*"):
suffix = candidate.name.removeprefix(prefix)
try:
chunk_index = float(suffix)
except ValueError:
continue
chunk_paths.append((chunk_index, candidate))
return [path for _, path in sorted(chunk_paths, key=lambda item: item[0])]
def discover_cached_segment_indexes(route: str,
cache_root: Path = COMMA_DATA_CACHE_ROOT,
max_seg: int = MAX_CACHED_ROUTE_SEGMENTS) -> list[int]:
found: list[int] = []
consecutive_misses = 0
for seg in range(max_seg + 1):
if cached_rlog_chunk_paths(route, seg, cache_root):
found.append(seg)
consecutive_misses = 0
elif found:
consecutive_misses += 1
if consecutive_misses >= MAX_CONSECUTIVE_CACHE_MISSES:
break
if not found:
raise FileNotFoundError(f"No cached rlogs found for {route} under {cache_root}")
return found
def select_segment_indexes(route: str, segment_slice: str | None, available_idxs: list[int]) -> list[int]:
if segment_slice is None:
return available_idxs
available_set = set(available_idxs)
if ":" not in segment_slice:
seg = int(segment_slice)
if seg not in available_set:
raise FileNotFoundError(f"Requested segment {seg} is not available in local cache for {route}")
return [seg]
raw_parts = segment_slice.split(":")
if len(raw_parts) > 3:
raise ValueError(f"Invalid segment slice: {segment_slice}")
parts = [int(part) if part else None for part in raw_parts]
while len(parts) < 3:
parts.append(None)
start, end, step = parts
max_seg = max(available_idxs)
if end is None or end < 0 or (start is not None and start < 0):
universe = list(range(max_seg + 1))
else:
universe = list(range(max(max_seg, end) + 1))
requested = universe[slice(start, end, step)]
missing = [seg for seg in requested if seg not in available_set]
if missing:
raise FileNotFoundError(f"Requested cached segments are unavailable for {route}: {missing[:8]}")
return requested
def rebuild_cached_rlogs(identifier: str,
cache_root: Path = COMMA_DATA_CACHE_ROOT,
out_root: Path = ROUTE_CACHE_REBUILD_ROOT) -> tuple[str, list[str]]:
route, segment_slice = parse_route_identifier(identifier)
available_idxs = discover_cached_segment_indexes(route, cache_root)
requested_idxs = select_segment_indexes(route, segment_slice, available_idxs)
rebuilt_paths: list[str] = []
for seg in requested_idxs:
chunk_paths = cached_rlog_chunk_paths(route, seg, cache_root)
if not chunk_paths:
raise FileNotFoundError(f"Missing cached rlog chunks for {route}/{seg}")
out_path = out_root / route / str(seg) / "rlog.zst"
expected_size = sum(chunk_path.stat().st_size for chunk_path in chunk_paths)
if not out_path.exists() or out_path.stat().st_size != expected_size:
out_path.parent.mkdir(parents=True, exist_ok=True)
with out_path.open("wb") as out_file:
for chunk_path in chunk_paths:
with chunk_path.open("rb") as chunk_file:
out_file.write(chunk_file.read())
rebuilt_paths.append(str(out_path))
return route, rebuilt_paths
def score_route(route: str, capture_events: bool = False, max_events: int = 200, use_cached_rlogs: bool = False) -> tuple[RouteMetrics, list[dict]]:
display_route = parse_route_identifier(route)[0] if use_cached_rlogs else route
metrics = RouteMetrics(route=display_route)
planner = None
toggles = default_toggles()
state: dict[str, object] = {}
@@ -159,7 +274,13 @@ def score_route(route: str, capture_events: bool = False, max_events: int = 200)
prev_plan_t: int | None = None
prev_a_target: float | None = None
for msg in LogReader(route, sort_by_time=True):
logreader_input: str | list[str]
if use_cached_rlogs:
_, logreader_input = rebuild_cached_rlogs(route)
else:
logreader_input = route
for msg in LogReader(logreader_input, sort_by_time=True):
which = msg.which()
if which == "carParams" and planner is None:
planner = LongitudinalPlanner(msg.carParams)
@@ -399,6 +520,8 @@ def parse_args() -> argparse.Namespace:
help="Total weight assigned to the priority subset")
parser.add_argument("--planner-profile", choices=("current", "legacy_panic_bypass"), default="current",
help="Planner behavior profile used for local comparison runs")
parser.add_argument("--cached-rlogs", action="store_true",
help="Replay from locally cached rlogs rebuilt from the download cache")
parser.add_argument("--json-out", type=str,
help="Optional local output path for JSON results")
parser.add_argument("--events-out", type=str,
@@ -412,7 +535,10 @@ def main() -> None:
args = parse_args()
configure_planner_profile(args.planner_profile)
scored = [score_route(route, capture_events=bool(args.events_out), max_events=args.max_events_per_route) for route in args.routes]
scored = [score_route(route,
capture_events=bool(args.events_out),
max_events=args.max_events_per_route,
use_cached_rlogs=args.cached_rlogs) for route in args.routes]
results = [result for result, _ in scored]
events = {route: route_events for (result, route_events), route in zip(scored, args.routes)}
payload = {