mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-08-31 13:13:44 +08:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fbcc0bed66 | |||
| 78aba03b4f | |||
| 2c002755c7 | |||
| 81d20d304f | |||
| d683da24ea | |||
| 4509316877 | |||
| 6a17743513 | |||
| e20ea5d0d1 | |||
| f20b256473 | |||
| a88da4071f |
@@ -221,6 +221,11 @@ struct StarPilotPlan @0xf98d843bfd7004a3 {
|
||||
trackingLead @36 :Bool;
|
||||
stopSignConfirmed @37 :Bool;
|
||||
pulseGlideCoasting @38 :Bool; # developer-only P&G phase for on-road status UI
|
||||
# Curve Speed Controller diagnostics, for tuning and rollout validation
|
||||
cscOverridden @39 :Bool; # driver cancelled this curve with RES+
|
||||
cscLearnedLatAccel @40 :Float32; # learned comfort at the current curvature, before margin
|
||||
cscBindingDistance @41 :Float32; # distance to the horizon point setting the target, m
|
||||
approachStopLength @42 :Float32; # pre-commit distance to a detected stop, m; 0 when off
|
||||
}
|
||||
|
||||
struct StarPilotRadarState @0xb86e6369214c01c8 {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 13 KiB |
@@ -169,12 +169,27 @@ CURVATURE_HOLD_OPPOSITE_RELEASE = 0.01 # 1/m
|
||||
CURVATURE_HOLD_CONFIRM_MIN = 0.003 # 1/m (~7 deg) of wound curvature before capture
|
||||
CURVATURE_HOLD_CONFIRM_SWEPT = 0.6 # rad of heading swept this blinker cycle; past this the push is exit-shaping, not initiation
|
||||
|
||||
# Suppress low-speed action spikes while the model's spatial path remains straight.
|
||||
TWITCH_GUARD_MAX_SPEED = 4.0
|
||||
TWITCH_GUARD_FADE_SPEED = 3.0
|
||||
TWITCH_GUARD_DURATION = 1.5
|
||||
TWITCH_GUARD_PLAN_RATIO = 4.0
|
||||
TWITCH_GUARD_FLOOR = 0.002
|
||||
TWITCH_GUARD_STRAIGHT_LO = 0.005
|
||||
TWITCH_GUARD_STRAIGHT_HI = 0.014
|
||||
TWITCH_GUARD_MIN_REACH = 12.0
|
||||
|
||||
|
||||
def _plan_circle_curvature(xs, ys, lookahead: float) -> float:
|
||||
# curvature of the circle through the origin, tangent to the car's heading, passing
|
||||
# through the plan point ~lookahead meters ahead: kappa = 2y / (x^2 + y^2)
|
||||
# Fit curvature through the plan point at the requested lookahead.
|
||||
px, py = 0.0, 0.0
|
||||
for x, y in zip(xs, ys):
|
||||
for x, y in zip(xs, ys, strict=False):
|
||||
try:
|
||||
x, y = float(x), float(y)
|
||||
except (TypeError, ValueError, OverflowError):
|
||||
return 0.0
|
||||
if not (math.isfinite(x) and math.isfinite(y)):
|
||||
return 0.0
|
||||
px, py = x, y
|
||||
if math.hypot(x, y) >= lookahead:
|
||||
break
|
||||
@@ -185,11 +200,7 @@ def _plan_circle_curvature(xs, ys, lookahead: float) -> float:
|
||||
|
||||
|
||||
def _plan_dual_probe(model_v2, d_near: float, d_far: float) -> float:
|
||||
# Min-magnitude of a near and a far circle fit. The far probe alone assumes the turn
|
||||
# starts immediately, which over-winds wide turns whose arc begins several meters out
|
||||
# (wide multi-lane lefts): the near probe reads ~straight there and only grows as the
|
||||
# car approaches the arc, so the readout self-scales to the turn geometry. Sign
|
||||
# disagreement means no coherent turn ahead: contribute nothing.
|
||||
# Use the smaller magnitude of near and far probes to avoid early turn bias.
|
||||
xs, ys = model_v2.position.x, model_v2.position.y
|
||||
near = _plan_circle_curvature(xs, ys, d_near)
|
||||
far = _plan_circle_curvature(xs, ys, d_far)
|
||||
@@ -222,8 +233,52 @@ def get_plan_turn_onset_dist(model_v2) -> float:
|
||||
|
||||
|
||||
def get_plan_reach(model_v2) -> float:
|
||||
xs = model_v2.position.x
|
||||
return xs[-1] if len(xs) else 0.0
|
||||
try:
|
||||
xs = model_v2.position.x
|
||||
return float(xs[-1]) if len(xs) else 0.0
|
||||
except (AttributeError, IndexError, TypeError, ValueError, OverflowError):
|
||||
return 0.0
|
||||
|
||||
|
||||
def _plan_positions_are_finite(model_v2) -> bool:
|
||||
try:
|
||||
xs, ys = model_v2.position.x, model_v2.position.y
|
||||
return len(xs) == len(ys) and all(
|
||||
math.isfinite(float(x)) and math.isfinite(float(y)) for x, y in zip(xs, ys, strict=True)
|
||||
)
|
||||
except (AttributeError, TypeError, ValueError, OverflowError):
|
||||
return False
|
||||
|
||||
|
||||
def limit_curvature_to_plan(model_v2, curvature: float, v_ego: float) -> float:
|
||||
if not (math.isfinite(curvature) and math.isfinite(v_ego)):
|
||||
return curvature
|
||||
if v_ego >= TWITCH_GUARD_MAX_SPEED or curvature == 0.0:
|
||||
return curvature
|
||||
if not _plan_positions_are_finite(model_v2):
|
||||
return curvature
|
||||
reach = get_plan_reach(model_v2)
|
||||
if not math.isfinite(reach) or reach < TWITCH_GUARD_MIN_REACH:
|
||||
return curvature
|
||||
plan = abs(_plan_circle_curvature(model_v2.position.x, model_v2.position.y,
|
||||
CURVATURE_HOLD_PLAN_LOOKAHEAD_FAR))
|
||||
if not math.isfinite(plan):
|
||||
return curvature
|
||||
straightness = (plan - TWITCH_GUARD_STRAIGHT_LO) / (TWITCH_GUARD_STRAIGHT_HI - TWITCH_GUARD_STRAIGHT_LO)
|
||||
limit = max(TWITCH_GUARD_PLAN_RATIO * plan * min(max(straightness, 0.0), 1.0), TWITCH_GUARD_FLOOR)
|
||||
if abs(curvature) <= limit:
|
||||
return curvature
|
||||
fade = (TWITCH_GUARD_MAX_SPEED - v_ego) / (TWITCH_GUARD_MAX_SPEED - TWITCH_GUARD_FADE_SPEED)
|
||||
fade = min(max(fade, 0.0), 1.0)
|
||||
return curvature + (math.copysign(limit, curvature) - curvature) * fade
|
||||
|
||||
|
||||
def update_twitch_guard(remaining: float, v_ego: float, standstill: bool) -> float:
|
||||
if not (math.isfinite(remaining) and math.isfinite(v_ego)):
|
||||
return 0.0
|
||||
if standstill or abs(v_ego) <= 0.3:
|
||||
return TWITCH_GUARD_DURATION
|
||||
return max(remaining - DT_CTRL, 0.0)
|
||||
|
||||
|
||||
def get_control_lateral_smooth_seconds(brand: str, v_ego: float, vehicle_smooth_seconds: float) -> float:
|
||||
@@ -346,6 +401,7 @@ class Controls:
|
||||
self.turn_hold_handoff_t = 0.0
|
||||
self.turn_hold_done = False
|
||||
self.turn_blinker_swept = 0.0
|
||||
self.twitch_guard_remaining = 0.0
|
||||
self.kona_non_scc_lateral_active = False
|
||||
|
||||
self.pose_calibrator = PoseCalibrator()
|
||||
@@ -401,6 +457,7 @@ class Controls:
|
||||
|
||||
def state_control(self):
|
||||
CS = self.sm['carState']
|
||||
self.twitch_guard_remaining = update_twitch_guard(self.twitch_guard_remaining, CS.vEgo, CS.standstill)
|
||||
|
||||
# Update VehicleModel
|
||||
lp = self.sm['liveParameters']
|
||||
@@ -460,7 +517,11 @@ class Controls:
|
||||
# EcuDisableFailed is set when car started in READY mode (ECU disable was rejected)
|
||||
# Disable longitudinal so stock ACC works instead
|
||||
self.update_ecu_disable_failed()
|
||||
CC.longActive = CC.enabled and not any(e.overrideLongitudinal for e in self.sm['onroadEvents']) and not self.sm['starpilotCarState'].pauseLongitudinal and self.CP.openpilotLongitudinalControl and not self.ecu_disable_failed
|
||||
CC.longActive = (
|
||||
CC.enabled and not any(e.overrideLongitudinal for e in self.sm['onroadEvents']) and
|
||||
not self.sm['starpilotCarState'].pauseLongitudinal and self.CP.openpilotLongitudinalControl and
|
||||
not self.ecu_disable_failed
|
||||
)
|
||||
|
||||
actuators = CC.actuators
|
||||
actuators.longControlState = self.LoC.long_control_state
|
||||
@@ -499,6 +560,9 @@ class Controls:
|
||||
# here is positive for RIGHT turns (pauseturn log: left turn at +148 deg steering
|
||||
# angle logs desiredCurvature -0.07), so the blinker maps right=+1, left=-1.
|
||||
blinker_dir = float(CS.rightBlinker) - float(CS.leftBlinker)
|
||||
if (CC.latActive and self.twitch_guard_remaining > 0.0 and
|
||||
blinker_dir == 0.0 and self.turn_hold_curvature == 0.0):
|
||||
new_desired_curvature = limit_curvature_to_plan(model_v2, new_desired_curvature, CS.vEgo)
|
||||
# heading swept in the blinker's direction over the whole blinker cycle (any speed):
|
||||
# discriminates a turn not yet made from one being exited (see the re-arm below)
|
||||
if blinker_dir == 0.0:
|
||||
|
||||
@@ -85,7 +85,8 @@ class LaneCenteringController:
|
||||
def _covers(x, distance: float) -> bool:
|
||||
return bool(x[0] <= distance <= x[-1])
|
||||
|
||||
def _raw_correction(self, model_v2, v_ego: float, offset: float, e2e_authority: float) -> tuple[bool, float]:
|
||||
@staticmethod
|
||||
def _raw_correction(model_v2, v_ego: float, offset: float, e2e_authority: float) -> tuple[bool, float]:
|
||||
try:
|
||||
lane_lines = model_v2.laneLines
|
||||
probs = np.asarray(model_v2.laneLineProbs, dtype=float)
|
||||
@@ -105,11 +106,13 @@ class LaneCenteringController:
|
||||
right_y = np.asarray(lane_lines[2].y, dtype=float)
|
||||
pos_x = np.asarray(model_v2.position.x, dtype=float)
|
||||
pos_y = np.asarray(model_v2.position.y, dtype=float)
|
||||
if not (self._valid_path(left_x, left_y) and self._valid_path(right_x, right_y) and self._valid_path(pos_x, pos_y)):
|
||||
if not (LaneCenteringController._valid_path(left_x, left_y) and
|
||||
LaneCenteringController._valid_path(right_x, right_y) and
|
||||
LaneCenteringController._valid_path(pos_x, pos_y)):
|
||||
return False, 0.0
|
||||
|
||||
lookahead = float(np.clip(v_ego, 8.0, 35.0))
|
||||
if not all(self._covers(x, lookahead) for x in (left_x, right_x, pos_x)):
|
||||
if not all(LaneCenteringController._covers(x, lookahead) for x in (left_x, right_x, pos_x)):
|
||||
return False, 0.0
|
||||
|
||||
left = float(np.interp(lookahead, left_x, left_y))
|
||||
@@ -130,7 +133,7 @@ class LaneCenteringController:
|
||||
|
||||
try:
|
||||
pos_y_std = np.asarray(model_v2.position.yStd, dtype=float)
|
||||
if self._valid_path(pos_x, pos_y_std):
|
||||
if LaneCenteringController._valid_path(pos_x, pos_y_std):
|
||||
path_std = float(np.interp(lookahead, pos_x, pos_y_std))
|
||||
if 0.0 <= path_std <= _E2E_MAX_PATH_STD:
|
||||
break_in = np.clip(
|
||||
@@ -145,3 +148,43 @@ class LaneCenteringController:
|
||||
return True, float(2.0 * error / lookahead ** 2)
|
||||
except (AttributeError, IndexError, TypeError, ValueError):
|
||||
return False, 0.0
|
||||
|
||||
|
||||
def get_raw_lane_centering_correction(model_v2, v_ego: float, offset: float,
|
||||
e2e_authority: float) -> tuple[bool, float]:
|
||||
"""Return the instantaneous lane-centering correction without controller filtering."""
|
||||
return LaneCenteringController._raw_correction(model_v2, v_ego, offset, e2e_authority)
|
||||
|
||||
|
||||
def get_lane_centering_visual_direction(model_v2, v_ego: float, offset: float, e2e_authority: float,
|
||||
enabled: bool, lat_active: bool, pause_on_signal: bool = False,
|
||||
turn_signal_active: bool = False,
|
||||
applied_correction: float | None = None) -> int:
|
||||
"""Return 1 for a right correction, -1 for left, and 0 when no correction is active."""
|
||||
if not enabled or not lat_active or (pause_on_signal and turn_signal_active):
|
||||
return 0
|
||||
|
||||
try:
|
||||
v_ego = float(v_ego)
|
||||
offset = float(offset)
|
||||
e2e_authority = float(e2e_authority)
|
||||
if not np.isfinite([v_ego, offset, e2e_authority]).all() or v_ego < _MIN_V_EGO:
|
||||
return 0
|
||||
if model_v2.meta.laneChangeState != log.LaneChangeState.off:
|
||||
return 0
|
||||
except (AttributeError, TypeError, ValueError):
|
||||
return 0
|
||||
|
||||
valid, correction = get_raw_lane_centering_correction(
|
||||
model_v2,
|
||||
v_ego,
|
||||
float(np.clip(offset, -_MAX_OFFSET, _MAX_OFFSET)),
|
||||
float(np.clip(e2e_authority, 0.0, 1.0)),
|
||||
)
|
||||
if not valid or not np.isfinite(correction):
|
||||
return 0
|
||||
if correction == 0.0:
|
||||
if applied_correction is None or not np.isfinite(applied_correction) or applied_correction == 0.0:
|
||||
return 0
|
||||
correction = applied_correction
|
||||
return 1 if correction > 0.0 else -1
|
||||
|
||||
@@ -9,6 +9,7 @@ from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
from openpilot.starpilot.common.model_versions import is_tinygrad_model_version
|
||||
from openpilot.starpilot.controls.lib.starpilot_vcruise import FT_TO_M, OFFSET_FT_MAX, OFFSET_FT_MIN
|
||||
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import desired_follow_distance
|
||||
@@ -381,6 +382,11 @@ def get_vehicle_min_accel(CP, v_ego):
|
||||
|
||||
# Restored planner constants retained by CEM, stop, and departure paths.
|
||||
A_CRUISE_MIN = -1.0
|
||||
# The stop distance runs ~9 m long through the mid-approach, which leaves the obstacle slack
|
||||
# so it stays silent and deceleration sags. Multiplicative so the trim scales with what is
|
||||
# left. Note the car parks where the obstacle sits, so this is also a placement bias — 0.85
|
||||
# stopped ~4.6 m short, 0.93 ~1.6 m.
|
||||
FORCE_STOP_OBSTACLE_TRIM = 0.93
|
||||
STANDSTILL_LEAD_CREEP_RELEASE_MIN_LEAD_SPEED = 0.25
|
||||
STANDSTILL_LEAD_CREEP_RELEASE_MIN_LEAD_ACCEL = 0.08
|
||||
STANDSTILL_LEAD_CREEP_RELEASE_MIN_GAP_MARGIN = 0.1
|
||||
@@ -2217,8 +2223,17 @@ class LongitudinalPlanner:
|
||||
force_stop_x = None
|
||||
force_stop_handoff_m = get_force_stop_handoff_distance(self.CP.carFingerprint)
|
||||
if sm['starpilotPlan'].forcingStop and sm['starpilotPlan'].forcingStopLength > force_stop_handoff_m:
|
||||
stop_length = float(sm['starpilotPlan'].forcingStopLength)
|
||||
else:
|
||||
# pre-commit the envelope is only a speed ceiling, which the solver tracks with a lag;
|
||||
# getattr so a stale cereal build degrades to the old behaviour instead of raising
|
||||
stop_length = float(getattr(sm['starpilotPlan'], 'approachStopLength', 0.0))
|
||||
if stop_length > force_stop_handoff_m:
|
||||
# ForceStopDistanceOffset shifts the perceived line for the v_cruise ceiling, so it has
|
||||
# to shift the obstacle too or the slider barely moves anything now that stop_x leads.
|
||||
offset_ft = max(OFFSET_FT_MIN, min(OFFSET_FT_MAX, int(getattr(starpilot_toggles, 'force_stop_distance_offset', 0) or 0)))
|
||||
force_stop_x = (
|
||||
float(sm['starpilotPlan'].forcingStopLength) + STOP_DISTANCE +
|
||||
stop_length * FORCE_STOP_OBSTACLE_TRIM + offset_ft * FT_TO_M + STOP_DISTANCE +
|
||||
get_force_stop_distance_bias(self.CP.carFingerprint)
|
||||
)
|
||||
|
||||
|
||||
@@ -53,6 +53,8 @@ ADJACENT_STOP_REST_FRAMES = 15
|
||||
ADJACENT_STOP_MIN_Y = 1.8 # m — inside this is our own lane
|
||||
ADJACENT_STOP_MAX_Y = 7.5 # m — beyond this is roadside, not an adjacent lane
|
||||
ADJACENT_STOP_MAX_D = 110.0 # m
|
||||
ADJACENT_STOP_QUEUE_GAP_M = 5.0 # m — anything stopped beyond the furthest qualifier means
|
||||
# the bar is past it too, so the hint would stop us short
|
||||
|
||||
|
||||
class KalmanParams:
|
||||
@@ -179,6 +181,10 @@ class Track:
|
||||
if self.leadTrackID == self.identifier:
|
||||
return False
|
||||
|
||||
return self.in_adjacent_lane(model_data)
|
||||
|
||||
def in_adjacent_lane(self, model_data: capnp._DynamicStructReader):
|
||||
"""Lane geometry only, no deceleration history — also used to spot a queue ahead."""
|
||||
if not (ADJACENT_STOP_MIN_Y < abs(self.yRel) < ADJACENT_STOP_MAX_Y):
|
||||
return False
|
||||
|
||||
@@ -398,9 +404,10 @@ def get_adjacent_lead(tracks: dict[int, Track], standstill: bool, model_data: ca
|
||||
def get_adjacent_stopped(tracks: dict[int, Track], model_data: capnp._DynamicStructReader) -> dict[str, Any]:
|
||||
"""Stop-line hint: a vehicle that decelerated to a stop in a neighbouring lane.
|
||||
|
||||
Takes the FARTHEST qualifying vehicle: in a queue the front car sits at the bar and the
|
||||
rest are closer to us, so the nearest one underestimates the distance. The consumer only
|
||||
shortens with this, so underestimating is the harmful direction.
|
||||
Takes the FARTHEST qualifying vehicle, then drops the hint entirely if a queue reaches
|
||||
past it. Cars already stopped when we acquire them never show the moving -> stopped
|
||||
transition, so the qualifying set is biased toward the back of a line; without this the
|
||||
hint marks a mid-queue bumper and stops us short of the bar.
|
||||
"""
|
||||
if len(model_data.laneLines) < 4:
|
||||
return {'status': False}
|
||||
@@ -410,6 +417,11 @@ def get_adjacent_stopped(tracks: dict[int, Track], model_data: capnp._DynamicStr
|
||||
return {'status': False}
|
||||
|
||||
furthest = max(candidates, key=lambda c: c.dRel)
|
||||
for c in tracks.values():
|
||||
if (c.dRel > furthest.dRel + ADJACENT_STOP_QUEUE_GAP_M and
|
||||
abs(c.vLead) < ADJACENT_STOP_REST_V and
|
||||
c.in_adjacent_lane(model_data)):
|
||||
return {'status': False}
|
||||
return {
|
||||
'status': True,
|
||||
'dRel': float(furthest.dRel),
|
||||
|
||||
@@ -3,7 +3,7 @@ from types import SimpleNamespace
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from openpilot.selfdrive.controls.lib.lane_centering import LaneCenteringController
|
||||
from openpilot.selfdrive.controls.lib.lane_centering import LaneCenteringController, get_lane_centering_visual_direction
|
||||
|
||||
|
||||
_V_EGO = 20.0
|
||||
@@ -174,3 +174,20 @@ def test_correction_is_smoothed_and_capped():
|
||||
_, steady = _converge(model, authority=0.0)
|
||||
assert 0.0 < first < steady
|
||||
assert np.isclose(steady, 0.004 * 0.30, atol=1e-6)
|
||||
|
||||
|
||||
def test_visual_direction_matches_curvature_sign():
|
||||
# Positive curvature is right in this tree's convention.
|
||||
assert get_lane_centering_visual_direction(_model(left=-1.5, right=2.1), _V_EGO, 0.0, 0.0, True, True) == 1
|
||||
assert get_lane_centering_visual_direction(_model(left=-2.1, right=1.5), _V_EGO, 0.0, 0.0, True, True) == -1
|
||||
|
||||
|
||||
def test_visual_direction_requires_both_primary_lane_lines():
|
||||
model = _model(left=-1.5, right=2.1)
|
||||
model.laneLineProbs[2] = 0.2
|
||||
assert get_lane_centering_visual_direction(model, _V_EGO, 0.0, 0.0, True, True) == 0
|
||||
|
||||
|
||||
def test_visual_direction_uses_filtered_correction_in_deadband():
|
||||
model = _model()
|
||||
assert get_lane_centering_visual_direction(model, _V_EGO, 0.0, 0.0, True, True, applied_correction=0.001) == 1
|
||||
|
||||
@@ -526,6 +526,7 @@ def make_sm(v_ego: float, desired_accel: float, min_accel: float, *, experimenta
|
||||
forcingStop=False,
|
||||
redLight=False,
|
||||
forcingStopLength=2,
|
||||
approachStopLength=0.0,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -2990,6 +2991,39 @@ def test_modeld_action_uses_current_action_head_scaling_for_v15(monkeypatch):
|
||||
assert not action.shouldStop
|
||||
|
||||
|
||||
def test_modeld_action_uses_current_action_head_scaling_for_v16(monkeypatch):
|
||||
monkeypatch.setenv("DEBUG", "0")
|
||||
fake_commonmodel = types.ModuleType("openpilot.selfdrive.modeld.models.commonmodel_pyx")
|
||||
fake_commonmodel.DrivingModelFrame = object
|
||||
fake_commonmodel.CLContext = object
|
||||
monkeypatch.setitem(sys.modules, fake_commonmodel.__name__, fake_commonmodel)
|
||||
|
||||
from openpilot.selfdrive.modeld import modeld
|
||||
|
||||
prev_action = log.ModelDataV2.Action.new_message()
|
||||
prev_action.desiredCurvature = 0.05
|
||||
prev_action.desiredAcceleration = -0.2
|
||||
toggles = SimpleNamespace(vEgoStopping=0.42)
|
||||
|
||||
action = modeld.get_action_from_model(
|
||||
{"action": np.array([[12.0, -0.8]], dtype=np.float32)},
|
||||
prev_action,
|
||||
lat_action_t=0.2,
|
||||
long_action_t=0.73,
|
||||
v_ego=5.0,
|
||||
mlsim=True,
|
||||
is_v9=False,
|
||||
is_v14=False,
|
||||
is_v15=False,
|
||||
starpilot_toggles=toggles,
|
||||
is_v16=True,
|
||||
)
|
||||
|
||||
assert action.desiredCurvature == pytest.approx(modeld.smooth_value(0.48, prev_action.desiredCurvature, modeld.LAT_SMOOTH_SECONDS))
|
||||
assert action.desiredAcceleration < -0.2
|
||||
assert not action.shouldStop
|
||||
|
||||
|
||||
def test_publish_force_stop_handoff_sets_should_stop_when_vcruise_zero():
|
||||
class FakePM:
|
||||
def __init__(self):
|
||||
|
||||
@@ -7,7 +7,9 @@ from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.starpilot.common.starpilot_variables import PLANNER_TIME
|
||||
from openpilot.starpilot.controls.lib.curve_speed_controller import CSC_MAX_DECEL_RATE, CurveSpeedController
|
||||
from openpilot.starpilot.controls.lib.starpilot_vcruise import (
|
||||
FORCE_STOP_CAP_SLACK_M,
|
||||
FORCE_STOP_TURN_VETO_STOP_SEEN_HOLD_TIME,
|
||||
STANDSTILL_FORCE_STOP_LIGHT_HOLD_TIME,
|
||||
StarPilotVCruise,
|
||||
get_active_slc_control_target,
|
||||
get_lead_veto_distance,
|
||||
@@ -57,6 +59,8 @@ def make_vcruise(*, red_light=False, raw_model_stopped=False, forcing_stop=False
|
||||
vcruise.forcing_stop = forcing_stop
|
||||
vcruise.force_stop_timer = 1.0 if forcing_stop else 0.0
|
||||
vcruise.tracked_model_length = 0.0 if forcing_stop else planner.model_length
|
||||
# what the not-committed branch would have left behind on the frame before commit
|
||||
vcruise.force_stop_distance_cap = planner.model_length
|
||||
return planner, vcruise
|
||||
|
||||
|
||||
@@ -521,14 +525,15 @@ def test_force_stop_stays_committed_while_moving_even_if_scene_opens():
|
||||
|
||||
def test_force_stop_reanchors_when_model_reopens_path_without_stop_action():
|
||||
planner, vcruise = make_vcruise(red_light=False, raw_model_stopped=False, forcing_stop=True)
|
||||
planner.model_length = 40.0
|
||||
vcruise.tracked_model_length = 10.0
|
||||
planner.model_length = 90.0
|
||||
vcruise.tracked_model_length = 60.0
|
||||
vcruise.force_stop_distance_cap = 90.0
|
||||
sm = make_sm(standstill=False)
|
||||
sm["modelV2"] = SimpleNamespace(action=SimpleNamespace(shouldStop=False))
|
||||
|
||||
result = update_vcruise(vcruise, sm, make_toggles(), now=0.0, v_ego=1.5)
|
||||
|
||||
assert vcruise.tracked_model_length == pytest.approx(40.0)
|
||||
assert vcruise.tracked_model_length == pytest.approx(90.0)
|
||||
assert result > 5.0
|
||||
|
||||
|
||||
@@ -560,6 +565,49 @@ def test_santa_fe_force_stop_holds_through_low_speed_detector_dropout():
|
||||
assert result == pytest.approx(0.0)
|
||||
|
||||
|
||||
def test_force_stop_does_not_reanchor_inside_reanchor_floor():
|
||||
planner, vcruise = make_vcruise(red_light=False, raw_model_stopped=False, forcing_stop=True)
|
||||
planner.model_length = 90.0
|
||||
vcruise.tracked_model_length = 25.0
|
||||
sm = make_sm(standstill=False)
|
||||
sm["modelV2"] = SimpleNamespace(action=SimpleNamespace(shouldStop=False))
|
||||
|
||||
update_vcruise(vcruise, sm, make_toggles(), now=0.0, v_ego=1.5)
|
||||
|
||||
assert vcruise.tracked_model_length < 25.0
|
||||
|
||||
|
||||
def test_force_stop_reanchor_bounded_by_distance_driven():
|
||||
# The line can't recede: a ballooning horizon may not push the stop past where it was at
|
||||
# commit minus the distance driven since.
|
||||
planner, vcruise = make_vcruise(red_light=False, raw_model_stopped=False, forcing_stop=True)
|
||||
planner.model_length = 200.0
|
||||
vcruise.tracked_model_length = 60.0
|
||||
vcruise.force_stop_distance_cap = 70.0
|
||||
sm = make_sm(standstill=False)
|
||||
sm["modelV2"] = SimpleNamespace(action=SimpleNamespace(shouldStop=False))
|
||||
|
||||
update_vcruise(vcruise, sm, make_toggles(), now=0.0, v_ego=10.0)
|
||||
|
||||
assert vcruise.tracked_model_length <= 70.0 + FORCE_STOP_CAP_SLACK_M
|
||||
assert vcruise.tracked_model_length < 100.0 # nowhere near the 200 m the horizon claimed
|
||||
|
||||
|
||||
def test_force_stop_cap_slack_tapers_near_the_line():
|
||||
# Slack protects against an under-read at commit; held near the line it would just aim the
|
||||
# solver that far past the stop bar.
|
||||
planner, vcruise = make_vcruise(red_light=False, raw_model_stopped=False, forcing_stop=True)
|
||||
planner.model_length = 200.0
|
||||
vcruise.tracked_model_length = 60.0
|
||||
vcruise.force_stop_distance_cap = 12.0
|
||||
sm = make_sm(standstill=False)
|
||||
sm["modelV2"] = SimpleNamespace(action=SimpleNamespace(shouldStop=False))
|
||||
|
||||
update_vcruise(vcruise, sm, make_toggles(), now=0.0, v_ego=5.0)
|
||||
|
||||
assert vcruise.tracked_model_length < 12.0 + FORCE_STOP_CAP_SLACK_M / 2.0
|
||||
|
||||
|
||||
def test_force_stop_does_not_reanchor_committed_model_stop():
|
||||
planner, vcruise = make_vcruise(red_light=False, raw_model_stopped=False, forcing_stop=True)
|
||||
planner.model_length = 40.0
|
||||
@@ -841,16 +889,16 @@ def test_standstill_light_hold_expires_and_does_not_rearm_from_stopped_model():
|
||||
assert update_vcruise(vcruise, sm, toggles, now=0.0) == pytest.approx(0.0)
|
||||
assert vcruise.standstill_force_stop_reason == "light"
|
||||
|
||||
assert update_vcruise(vcruise, sm, toggles, now=4.9) == pytest.approx(0.0)
|
||||
assert update_vcruise(vcruise, sm, toggles, now=STANDSTILL_FORCE_STOP_LIGHT_HOLD_TIME - 0.1) == pytest.approx(0.0)
|
||||
assert vcruise.forcing_stop
|
||||
|
||||
assert update_vcruise(vcruise, sm, toggles, now=5.1) == pytest.approx(20.0)
|
||||
assert update_vcruise(vcruise, sm, toggles, now=STANDSTILL_FORCE_STOP_LIGHT_HOLD_TIME + 0.1) == pytest.approx(20.0)
|
||||
assert not vcruise.forcing_stop
|
||||
assert not vcruise.standstill_force_stop_hold
|
||||
|
||||
# The red-light model remains stopped, but Force Stop must stay released so
|
||||
# Experimental Mode can own the red-to-green departure.
|
||||
assert update_vcruise(vcruise, sm, toggles, now=5.2) == pytest.approx(20.0)
|
||||
assert update_vcruise(vcruise, sm, toggles, now=STANDSTILL_FORCE_STOP_LIGHT_HOLD_TIME + 0.2) == pytest.approx(20.0)
|
||||
assert not vcruise.forcing_stop
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,40 @@
|
||||
import math
|
||||
import types
|
||||
|
||||
from cereal import car
|
||||
|
||||
import pytest
|
||||
|
||||
from openpilot.selfdrive.controls.controlsd import get_control_lateral_smooth_seconds, turn_lead_allowed
|
||||
from openpilot.common.realtime import DT_CTRL
|
||||
from openpilot.selfdrive.controls.controlsd import (
|
||||
TWITCH_GUARD_DURATION,
|
||||
TWITCH_GUARD_FLOOR,
|
||||
TWITCH_GUARD_MAX_SPEED,
|
||||
get_control_lateral_smooth_seconds,
|
||||
limit_curvature_to_plan,
|
||||
turn_lead_allowed,
|
||||
update_twitch_guard,
|
||||
)
|
||||
|
||||
|
||||
LateralControlMode = car.CarControl.Actuators.LateralControlMode
|
||||
|
||||
|
||||
def _plan(xs, ys):
|
||||
return types.SimpleNamespace(position=types.SimpleNamespace(x=xs, y=ys))
|
||||
|
||||
|
||||
def _arc_plan(radius, n=200):
|
||||
return _plan([radius * math.sin(i / n) for i in range(n)],
|
||||
[radius * (1.0 - math.cos(i / n)) for i in range(n)])
|
||||
|
||||
|
||||
STRAIGHT_PLAN = _plan([i * 0.5 for i in range(200)], [0.0] * 200)
|
||||
STANDSTILL_STUB_PLAN = _plan([0.0, 0.3], [0.0, 0.0])
|
||||
TURN_PLAN = _arc_plan(30.0)
|
||||
GENTLE_BEND_PLAN = _arc_plan(143.0)
|
||||
|
||||
|
||||
def test_turn_lead_is_suppressed_only_during_applied_angle_control():
|
||||
assert not turn_lead_allowed("rivian", LateralControlMode.angle)
|
||||
assert turn_lead_allowed("rivian", LateralControlMode.torque)
|
||||
@@ -37,3 +64,79 @@ def test_subaru_control_smoothing_uses_vehicle_schedule(v_ego, expected):
|
||||
])
|
||||
def test_rivian_control_smoothing_remains_speed_scheduled(v_ego, expected):
|
||||
assert get_control_lateral_smooth_seconds("rivian", v_ego, 0.4) == pytest.approx(expected)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("curvature", [0.0155, -0.0155])
|
||||
def test_twitch_against_a_straight_plan_is_clamped_to_the_floor(curvature):
|
||||
guarded = limit_curvature_to_plan(STRAIGHT_PLAN, curvature, 1.2)
|
||||
assert abs(guarded) == pytest.approx(TWITCH_GUARD_FLOOR)
|
||||
assert math.copysign(1.0, guarded) == math.copysign(1.0, curvature)
|
||||
|
||||
|
||||
def test_command_already_below_the_floor_is_untouched():
|
||||
assert limit_curvature_to_plan(STRAIGHT_PLAN, 0.0015, 1.2) == pytest.approx(0.0015)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("v_ego", [TWITCH_GUARD_MAX_SPEED, 6.0, 30.0])
|
||||
def test_guard_is_inactive_above_its_speed_band(v_ego):
|
||||
assert limit_curvature_to_plan(STRAIGHT_PLAN, 0.0155, v_ego) == pytest.approx(0.0155)
|
||||
|
||||
|
||||
def test_guard_fades_out_across_the_speed_band():
|
||||
full = limit_curvature_to_plan(STRAIGHT_PLAN, 0.0155, 1.2)
|
||||
half = limit_curvature_to_plan(STRAIGHT_PLAN, 0.0155, 3.5)
|
||||
assert full < half < 0.0155
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ratio", [0.8, 1.0, 2.0, 3.0])
|
||||
def test_real_turns_tracking_their_own_plan_are_untouched(ratio):
|
||||
action = (1.0 / 30.0) * ratio
|
||||
assert limit_curvature_to_plan(TURN_PLAN, action, 1.2) == pytest.approx(action)
|
||||
|
||||
|
||||
def test_a_barely_bending_plan_does_not_license_a_large_command():
|
||||
guarded = limit_curvature_to_plan(GENTLE_BEND_PLAN, 0.0155, 1.2)
|
||||
assert TWITCH_GUARD_FLOOR < guarded < 0.008
|
||||
|
||||
|
||||
@pytest.mark.parametrize("plan", [STANDSTILL_STUB_PLAN, _plan([], [])])
|
||||
def test_guard_stands_down_when_the_plan_is_too_short_to_judge(plan):
|
||||
assert limit_curvature_to_plan(plan, 0.0155, 0.4) == pytest.approx(0.0155)
|
||||
|
||||
|
||||
def test_zero_command_stays_zero():
|
||||
assert limit_curvature_to_plan(STRAIGHT_PLAN, 0.0, 1.2) == 0.0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad_plan", [
|
||||
_plan([0.0, math.nan, 20.0], [0.0, 0.0, 0.0]),
|
||||
_plan([0.0, math.inf, 20.0], [0.0, 0.0, 0.0]),
|
||||
_plan([0.0, 20.0], [0.0]),
|
||||
])
|
||||
def test_invalid_plan_data_leaves_curvature_untouched(bad_plan):
|
||||
assert limit_curvature_to_plan(bad_plan, 0.0155, 1.2) == pytest.approx(0.0155)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [math.nan, math.inf, -math.inf])
|
||||
def test_nonfinite_guard_inputs_disarm(value):
|
||||
assert update_twitch_guard(value, 1.0, False) == 0.0
|
||||
assert update_twitch_guard(TWITCH_GUARD_DURATION, value, False) == 0.0
|
||||
|
||||
|
||||
def test_twitch_guard_arms_at_standstill_or_creep_speed():
|
||||
assert update_twitch_guard(0.0, 0.0, True) == TWITCH_GUARD_DURATION
|
||||
assert update_twitch_guard(0.0, 0.3, False) == TWITCH_GUARD_DURATION
|
||||
|
||||
|
||||
def test_twitch_guard_decays_after_pullaway_and_expires():
|
||||
remaining = update_twitch_guard(0.0, 0.0, True)
|
||||
remaining = update_twitch_guard(remaining, 1.0, False)
|
||||
assert remaining == pytest.approx(TWITCH_GUARD_DURATION - DT_CTRL)
|
||||
|
||||
for _ in range(int(TWITCH_GUARD_DURATION / DT_CTRL) + 1):
|
||||
remaining = update_twitch_guard(remaining, 1.0, False)
|
||||
assert remaining == 0.0
|
||||
|
||||
|
||||
def test_twitch_guard_does_not_arm_while_moving():
|
||||
assert update_twitch_guard(0.0, 1.0, False) == 0.0
|
||||
|
||||
@@ -204,13 +204,14 @@ def _packed_policy_shapes(input_shapes, include_prev_feature=False):
|
||||
shapes[key] = tuple(shape)
|
||||
if include_prev_feature:
|
||||
features_shape = input_shapes["features_buffer"]
|
||||
shapes["prev_feat"] = (features_shape[0], features_shape[2])
|
||||
shapes["prev_feat"] = (features_shape[0], math.prod(features_shape[2:]))
|
||||
return shapes, [math.prod(shape) for shape in shapes.values()]
|
||||
|
||||
|
||||
def make_split_input_queues(vision_input_shapes, policy_input_shapes, frame_skip, device):
|
||||
queues, npy = make_warp_input_queues(vision_input_shapes, frame_skip, device)
|
||||
features_shape = policy_input_shapes["features_buffer"]
|
||||
feature_dim = math.prod(features_shape[2:])
|
||||
desire_key = _detect_desire_key(policy_input_shapes)
|
||||
desire_shape = policy_input_shapes[desire_key]
|
||||
packed_shapes, packed_sizes = _packed_policy_shapes(policy_input_shapes)
|
||||
@@ -224,7 +225,7 @@ def make_split_input_queues(vision_input_shapes, policy_input_shapes, frame_skip
|
||||
})
|
||||
queues.update({
|
||||
"feat_q": Tensor(
|
||||
np.zeros((frame_skip * (features_shape[1] - 1) + 1, features_shape[0], features_shape[2]), dtype=np.float32),
|
||||
np.zeros((frame_skip * (features_shape[1] - 1) + 1, features_shape[0], feature_dim), dtype=np.float32),
|
||||
device=device,
|
||||
).contiguous().realize(),
|
||||
"desire_q": Tensor(
|
||||
@@ -239,6 +240,7 @@ def make_split_input_queues(vision_input_shapes, policy_input_shapes, frame_skip
|
||||
def make_supercombo_input_queues(input_shapes, frame_skip, device):
|
||||
queues, npy = make_warp_input_queues(input_shapes, frame_skip, device)
|
||||
features_shape = input_shapes["features_buffer"]
|
||||
feature_dim = math.prod(features_shape[2:])
|
||||
desire_key = _detect_desire_key(input_shapes)
|
||||
desire_shape = input_shapes[desire_key]
|
||||
packed_shapes, packed_sizes = _packed_policy_shapes(input_shapes, include_prev_feature=True)
|
||||
@@ -252,7 +254,7 @@ def make_supercombo_input_queues(input_shapes, frame_skip, device):
|
||||
})
|
||||
queues.update({
|
||||
"feat_q": Tensor(
|
||||
np.zeros((frame_skip * features_shape[1], features_shape[0], features_shape[2]), dtype=np.float32),
|
||||
np.zeros((frame_skip * features_shape[1], features_shape[0], feature_dim), dtype=np.float32),
|
||||
device=device,
|
||||
).contiguous().realize(),
|
||||
"desire_q": Tensor(
|
||||
@@ -335,6 +337,7 @@ def make_run_split_policy(vision_runner, policy_runners, metadata, policy_order,
|
||||
vision_output = next(iter(vision_runner({road_key: img, wide_key: big_img}).values())).cast("float32")
|
||||
new_feature = vision_output[:, vision_features_slice].reshape(1, -1).unsqueeze(0)
|
||||
features_buffer = shift_and_sample(feat_q, new_feature, sample_skip_fn)
|
||||
features_buffer = features_buffer.reshape(policy_metadata["input_shapes"]["features_buffer"])
|
||||
|
||||
policy_inputs = {
|
||||
"features_buffer": features_buffer,
|
||||
@@ -388,6 +391,7 @@ def make_run_supercombo(model_runner, metadata, frame_skip, image_history_pipeli
|
||||
features_buffer = shift_and_sample(
|
||||
feat_q, previous_feature.reshape(1, 1, -1), sample_skip_fn,
|
||||
)
|
||||
features_buffer = features_buffer.reshape(input_shapes["features_buffer"])
|
||||
model_inputs = {
|
||||
road_key: img,
|
||||
wide_key: big_img,
|
||||
|
||||
+27
-12
@@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
from collections.abc import Callable
|
||||
import ctypes
|
||||
from functools import cached_property
|
||||
import os
|
||||
import struct
|
||||
@@ -159,8 +161,10 @@ class ChestnutState:
|
||||
if self.big and "AMD" in Device._opened_devices and self.sends % 100 == 1:
|
||||
try:
|
||||
smu = Device["AMD"].iface.dev_impl.smu
|
||||
metrics_t = smu.smu_mod.SmuMetricsExternal_t
|
||||
smu._send_msg(smu.smu_mod.PPSMC_MSG_TransferTableSmu2Dram, smu.smu_mod.TABLE_SMU_METRICS, timeout=100)
|
||||
metrics = smu.read_table(smu.smu_mod.SmuMetricsExternal_t, smu.smu_mod.TABLE_SMU_METRICS).SmuMetrics
|
||||
metrics_buf = bytearray(smu.adev.vram.view(smu.driver_table_paddr, ctypes.sizeof(metrics_t))[:])
|
||||
metrics = metrics_t.from_buffer(metrics_buf).SmuMetrics
|
||||
self.metrics = {
|
||||
"tempC": metrics.AvgTemperature[smu.smu_mod.TEMP_HOTSPOT],
|
||||
"memoryTempC": metrics.AvgTemperature[smu.smu_mod.TEMP_MEM],
|
||||
@@ -277,10 +281,11 @@ def _close_tinygrad_disk_cache_connection() -> None:
|
||||
def get_action_from_model(model_output: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action,
|
||||
lat_action_t: float, long_action_t: float, v_ego: float, mlsim: bool,
|
||||
is_v9: bool, is_v14: bool, is_v15: bool, starpilot_toggles,
|
||||
lat_smooth_seconds=LAT_SMOOTH_SECONDS, long_smooth_seconds=LONG_SMOOTH_SECONDS) -> log.ModelDataV2.Action:
|
||||
if is_v14 or is_v15:
|
||||
lat_smooth_seconds=LAT_SMOOTH_SECONDS, long_smooth_seconds=LONG_SMOOTH_SECONDS,
|
||||
is_v16: bool = False) -> log.ModelDataV2.Action:
|
||||
if is_v14 or is_v15 or is_v16:
|
||||
desired_curv_unscaled, desired_accel = model_output['action'][0]
|
||||
if is_v15:
|
||||
if is_v15 or is_v16:
|
||||
desired_curvature = float(desired_curv_unscaled) / max(1.0, v_ego) ** 2
|
||||
else:
|
||||
desired_curvature = float(desired_curv_unscaled) / 100.0
|
||||
@@ -464,6 +469,7 @@ class ModelState:
|
||||
self.is_v9 = self.policy_generation == "v9"
|
||||
self.is_v14 = self.policy_generation == "v14"
|
||||
self.is_v15 = self.policy_generation == "v15"
|
||||
self.is_v16 = self.policy_generation == "v16"
|
||||
self.mlsim = is_tinygrad_model_version(self.policy_generation)
|
||||
if write_model_version:
|
||||
params.put("ModelVersion", self.policy_generation)
|
||||
@@ -567,7 +573,8 @@ class ModelState:
|
||||
self._reset_state()
|
||||
|
||||
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
|
||||
inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None:
|
||||
inputs: dict[str, np.ndarray], prepare_only: bool,
|
||||
after_enqueue: Callable[[], None] | None = None) -> dict[str, np.ndarray] | None:
|
||||
frames: dict[str, Tensor] = {}
|
||||
for key, buf in bufs.items():
|
||||
ptr = np.frombuffer(buf.data, dtype=np.uint8).ctypes.data
|
||||
@@ -613,11 +620,12 @@ class ModelState:
|
||||
img=img,
|
||||
big_img=big_img,
|
||||
)
|
||||
if after_enqueue is not None:
|
||||
after_enqueue()
|
||||
outputs = [output.numpy().flatten() for output in output_tensors]
|
||||
|
||||
if self.uses_external_gpu and any(not np.isfinite(output).all() for output in outputs):
|
||||
cloudlog.error("external GPU model output not finite, dropping frame")
|
||||
return None
|
||||
raise RuntimeError("external GPU model output not finite")
|
||||
|
||||
if self.model_type == "supercombo":
|
||||
model_output = outputs[0]
|
||||
@@ -919,7 +927,17 @@ def main(demo=False):
|
||||
|
||||
mt1 = time.perf_counter()
|
||||
try:
|
||||
model_output = model.run(bufs, transforms, inputs, prepare_only)
|
||||
send_chestnut = (
|
||||
chestnut_state is not None and
|
||||
run_count % round(ModelConstants.MODEL_FREQ / SERVICE_LIST["chestnutState"].frequency) == 0
|
||||
)
|
||||
model_output = model.run(
|
||||
bufs,
|
||||
transforms,
|
||||
inputs,
|
||||
prepare_only,
|
||||
chestnut_state.send if send_chestnut else None,
|
||||
)
|
||||
except Exception:
|
||||
if not external_gpu_active or small_model is None:
|
||||
raise
|
||||
@@ -953,7 +971,7 @@ def main(demo=False):
|
||||
lat_action_t,
|
||||
long_action_t,
|
||||
v_ego, model.mlsim, model.is_v9, model.is_v14, model.is_v15, starpilot_toggles,
|
||||
lat_smooth_seconds, long_smooth_seconds,
|
||||
lat_smooth_seconds, long_smooth_seconds, is_v16=model.is_v16,
|
||||
)
|
||||
prev_action = action
|
||||
fill_model_msg(drivingdata_send, modelv2_send, model_output, action,
|
||||
@@ -982,9 +1000,6 @@ def main(demo=False):
|
||||
if sm.updated['starpilotPlan']:
|
||||
starpilot_toggles = get_starpilot_toggles(sm)
|
||||
|
||||
if chestnut_state is not None and run_count % round(ModelConstants.MODEL_FREQ / SERVICE_LIST["chestnutState"].frequency) == 0:
|
||||
chestnut_state.send()
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
import argparse
|
||||
|
||||
@@ -3,6 +3,7 @@ from types import MethodType
|
||||
from types import SimpleNamespace
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from openpilot.selfdrive.modeld import modeld
|
||||
from openpilot.selfdrive.modeld.helpers import dump_oob, load_oob, tinygrad_dev_config
|
||||
@@ -37,17 +38,18 @@ def test_external_gpu_uses_a_longer_load_watchdog():
|
||||
assert modeld.BIG_MODEL_RUN_WAIT_TIMEOUT_MS == 3000
|
||||
|
||||
|
||||
def test_external_gpu_signal_wait_matches_upstream_busy_poll():
|
||||
def test_external_gpu_signal_wait_yields_between_usb_polls(monkeypatch):
|
||||
from tinygrad.runtime import ops_amd
|
||||
|
||||
sleeps = []
|
||||
monkeypatch.setattr(ops_amd.time, "sleep", sleeps.append)
|
||||
signal = ops_amd.AMDSignal.__new__(ops_amd.AMDSignal)
|
||||
signal.should_return = False
|
||||
signal.owner = SimpleNamespace(is_usb=lambda: True, iface=SimpleNamespace(sleep=sleeps.append))
|
||||
signal.owner = SimpleNamespace(is_usb=lambda: True, iface=SimpleNamespace(sleep=lambda _: None))
|
||||
|
||||
signal._sleep(0)
|
||||
|
||||
assert sleeps == []
|
||||
assert sleeps == [ops_amd.AMD_USB_POLL_US / 1e6]
|
||||
|
||||
|
||||
def test_native_amd_signal_keeps_existing_short_wait_behavior():
|
||||
@@ -201,7 +203,7 @@ def test_external_gpu_load_finishes_before_native_model_can_start(monkeypatch):
|
||||
]
|
||||
|
||||
|
||||
def test_external_gpu_nonfinite_outputs_are_dropped_without_escalating(monkeypatch):
|
||||
def test_external_gpu_nonfinite_outputs_trigger_fallback(monkeypatch):
|
||||
class FakeTensor:
|
||||
@staticmethod
|
||||
def from_blob(*_args, **_kwargs):
|
||||
@@ -235,13 +237,7 @@ def test_external_gpu_nonfinite_outputs_are_dropped_without_escalating(monkeypat
|
||||
state.image_history_pipeline = modeld.IMAGE_HISTORY_IN_POLICY
|
||||
state.warp_enqueue = lambda **_kwargs: object()
|
||||
state.run_policy = lambda **_kwargs: (FakeOutput(),)
|
||||
state._reset_state = MethodType(
|
||||
lambda self: (_ for _ in ()).throw(AssertionError("upstream does not reset or escalate transient non-finite output")),
|
||||
state,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(modeld, "Tensor", FakeTensor)
|
||||
monkeypatch.setattr(modeld.cloudlog, "error", lambda *_args, **_kwargs: None)
|
||||
buffers = {
|
||||
"img": SimpleNamespace(data=bytearray(4)),
|
||||
"big_img": SimpleNamespace(data=bytearray(4)),
|
||||
@@ -252,8 +248,10 @@ def test_external_gpu_nonfinite_outputs_are_dropped_without_escalating(monkeypat
|
||||
}
|
||||
inputs = {"desire_pulse": np.zeros(8, dtype=np.float32)}
|
||||
|
||||
for _ in range(10):
|
||||
assert state.run(buffers, transforms, inputs, False) is None
|
||||
callbacks = []
|
||||
with pytest.raises(RuntimeError, match="external GPU model output not finite"):
|
||||
state.run(buffers, transforms, inputs, False, lambda: callbacks.append("sent"))
|
||||
assert callbacks == ["sent"]
|
||||
|
||||
|
||||
def test_out_of_band_artifact_round_trip():
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
|
||||
import pyray as rl
|
||||
@@ -18,6 +23,40 @@ from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets.scroller import NavScroller
|
||||
|
||||
UPDATER_TIMEOUT = 10.0
|
||||
FAST_UPDATE_HOLD_SECONDS = 0.8
|
||||
FAST_UPDATE_POLL_SECONDS = 0.5
|
||||
FAST_UPDATE_REQUEST_TIMEOUT = 5.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class FastUpdateDisplayState:
|
||||
stage: str = "idle"
|
||||
message: str = ""
|
||||
error: str = ""
|
||||
|
||||
|
||||
def _galaxy_api_url(path: str) -> str:
|
||||
port = os.getenv("SP_GALAXY_PORT", "8082")
|
||||
return f"http://127.0.0.1:{port}/api/update/fast{path}"
|
||||
|
||||
|
||||
def _galaxy_fast_update_request(path: str = "", method: str = "GET") -> dict:
|
||||
request = urllib.request.Request(_galaxy_api_url(path), method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=FAST_UPDATE_REQUEST_TIMEOUT) as response:
|
||||
payload = json.loads(response.read().decode("utf-8"))
|
||||
except urllib.error.HTTPError as error:
|
||||
try:
|
||||
payload = json.loads(error.read().decode("utf-8"))
|
||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||
payload = {}
|
||||
raise RuntimeError(payload.get("error") or str(error)) from error
|
||||
except (OSError, urllib.error.URLError, json.JSONDecodeError, UnicodeDecodeError) as error:
|
||||
raise RuntimeError(f"Galaxy fast update unavailable: {error}") from error
|
||||
|
||||
if not isinstance(payload, dict):
|
||||
raise RuntimeError("Galaxy returned an invalid fast update response")
|
||||
return payload
|
||||
|
||||
|
||||
def _split_description(desc: str) -> tuple[str, str, str, str] | None:
|
||||
@@ -101,6 +140,10 @@ class CheckUpdateButton(BigButton):
|
||||
self._waiting_for_updater_t: float | None = None
|
||||
self._hide_value_t: float | None = None
|
||||
self._state: UpdaterState = UpdaterState.IDLE
|
||||
self._press_start_t: float | None = None
|
||||
self._press_action = ""
|
||||
self._fast_update_state = FastUpdateDisplayState()
|
||||
self._fast_update_state_lock = threading.Lock()
|
||||
|
||||
ui_state.add_offroad_transition_callback(self.offroad_transition)
|
||||
|
||||
@@ -108,9 +151,24 @@ class CheckUpdateButton(BigButton):
|
||||
if ui_state.is_offroad():
|
||||
self.set_enabled(True)
|
||||
|
||||
def _handle_mouse_press(self, mouse_pos: MousePos):
|
||||
super()._handle_mouse_press(mouse_pos)
|
||||
self._press_start_t = rl.get_time()
|
||||
self._press_action = self.get_value()
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
held_for = 0.0 if self._press_start_t is None else rl.get_time() - self._press_start_t
|
||||
self._press_start_t = None
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
|
||||
if held_for >= FAST_UPDATE_HOLD_SECONDS:
|
||||
self._show_fast_update_confirmation()
|
||||
return
|
||||
|
||||
if self._get_fast_update_state().stage == "error":
|
||||
self._set_fast_update_state(stage="idle")
|
||||
return
|
||||
|
||||
if not system_time_valid():
|
||||
dlg = BigDialog("", tr("Please connect to Wi-Fi to update."))
|
||||
gui_app.push_widget(dlg)
|
||||
@@ -121,13 +179,75 @@ class CheckUpdateButton(BigButton):
|
||||
self.set_icon(self._txt_update_icon)
|
||||
|
||||
def run():
|
||||
if self.get_value() == "download update":
|
||||
if self._press_action == "download update":
|
||||
_request_update_download()
|
||||
else:
|
||||
_request_update_check()
|
||||
|
||||
threading.Thread(target=run, daemon=True).start()
|
||||
|
||||
def _show_fast_update_confirmation(self):
|
||||
if not system_time_valid():
|
||||
gui_app.push_widget(BigDialog("", tr("Please connect to Wi-Fi to update.")))
|
||||
return
|
||||
if ui_state.started:
|
||||
return
|
||||
|
||||
gui_app.push_widget(BigConfirmationDialog(
|
||||
"slide to\nfast update",
|
||||
self._txt_update_icon,
|
||||
self._start_fast_update,
|
||||
red=True,
|
||||
))
|
||||
|
||||
def _set_fast_update_state(self, *, stage: str, message: str = "", error: str = ""):
|
||||
with self._fast_update_state_lock:
|
||||
self._fast_update_state = FastUpdateDisplayState(stage, message, error)
|
||||
|
||||
def _get_fast_update_state(self) -> FastUpdateDisplayState:
|
||||
with self._fast_update_state_lock:
|
||||
state = self._fast_update_state
|
||||
return FastUpdateDisplayState(state.stage, state.message, state.error)
|
||||
|
||||
def _start_fast_update(self):
|
||||
if ui_state.started:
|
||||
return
|
||||
|
||||
state = self._get_fast_update_state()
|
||||
if state.stage not in ("idle", "error"):
|
||||
return
|
||||
|
||||
self._set_fast_update_state(stage="starting", message="starting fast update...")
|
||||
threading.Thread(target=self._run_fast_update, daemon=True).start()
|
||||
|
||||
def _run_fast_update(self):
|
||||
try:
|
||||
_galaxy_fast_update_request(method="POST")
|
||||
|
||||
while True:
|
||||
status = _galaxy_fast_update_request("/status")
|
||||
stage = str(status.get("stage") or "updating")
|
||||
error = str(status.get("lastError") or "").strip()
|
||||
message = str(
|
||||
status.get("progressDetail") or
|
||||
status.get("progressLabel") or
|
||||
status.get("message") or
|
||||
"fast update in progress..."
|
||||
).strip()
|
||||
|
||||
if error or stage == "error":
|
||||
self._set_fast_update_state(stage="error", message="fast update failed", error=error or message)
|
||||
return
|
||||
|
||||
self._set_fast_update_state(stage=stage, message=message)
|
||||
if not bool(status.get("running")):
|
||||
return
|
||||
time.sleep(FAST_UPDATE_POLL_SECONDS)
|
||||
except Exception as error:
|
||||
current = self._get_fast_update_state()
|
||||
if current.stage != "rebooting":
|
||||
self._set_fast_update_state(stage="error", message="fast update failed", error=str(error))
|
||||
|
||||
def set_value(self, value: str):
|
||||
super().set_value(value)
|
||||
self.set_text("" if value else "check for update")
|
||||
@@ -136,9 +256,25 @@ class CheckUpdateButton(BigButton):
|
||||
super()._update_state()
|
||||
|
||||
if ui_state.started:
|
||||
self._press_start_t = None
|
||||
self.set_enabled(False)
|
||||
return
|
||||
|
||||
fast_update_state = self._get_fast_update_state()
|
||||
if fast_update_state.stage != "idle":
|
||||
self.set_rotate_icon(fast_update_state.stage not in ("error", "rebooting"))
|
||||
if fast_update_state.stage == "error":
|
||||
self.set_enabled(True)
|
||||
self.set_value(fast_update_state.error or fast_update_state.message)
|
||||
elif fast_update_state.stage == "rebooting":
|
||||
self.set_enabled(False)
|
||||
self.set_value("update complete\nrebooting...")
|
||||
else:
|
||||
self.set_enabled(False)
|
||||
self.set_value(fast_update_state.message or "fast update in progress...")
|
||||
self.set_text("fast update")
|
||||
return
|
||||
|
||||
updater_state = ui_state.params.get("UpdaterState") or ""
|
||||
|
||||
if self._state == UpdaterState.WAITING_FOR_UPDATER:
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings import software
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
|
||||
|
||||
|
||||
def _make_button() -> software.CheckUpdateButton:
|
||||
button = object.__new__(software.CheckUpdateButton)
|
||||
button._press_start_t = None
|
||||
button._press_action = ""
|
||||
button._fast_update_state = software.FastUpdateDisplayState()
|
||||
button._fast_update_state_lock = software.threading.Lock()
|
||||
return button
|
||||
|
||||
|
||||
def test_fast_update_uses_local_galaxy_api(monkeypatch):
|
||||
monkeypatch.delenv("SP_GALAXY_PORT", raising=False)
|
||||
|
||||
assert software._galaxy_api_url("") == "http://127.0.0.1:8082/api/update/fast"
|
||||
assert software._galaxy_api_url("/status") == "http://127.0.0.1:8082/api/update/fast/status"
|
||||
|
||||
|
||||
def test_long_press_opens_fast_update_confirmation(monkeypatch):
|
||||
button = _make_button()
|
||||
button._press_start_t = 1.0
|
||||
confirmation_calls = []
|
||||
|
||||
monkeypatch.setattr(software.rl, "get_time", lambda: 1.0 + software.FAST_UPDATE_HOLD_SECONDS)
|
||||
monkeypatch.setattr(BigButton, "_handle_mouse_release", lambda *_args: None)
|
||||
monkeypatch.setattr(button, "_show_fast_update_confirmation", lambda: confirmation_calls.append(True))
|
||||
|
||||
button._handle_mouse_release(SimpleNamespace())
|
||||
|
||||
assert confirmation_calls == [True]
|
||||
|
||||
|
||||
def test_short_press_keeps_normal_updater_path(monkeypatch):
|
||||
button = _make_button()
|
||||
button._press_start_t = 1.0
|
||||
button._press_action = "download update"
|
||||
downloads = []
|
||||
confirmations = []
|
||||
|
||||
monkeypatch.setattr(software.rl, "get_time", lambda: 1.0 + software.FAST_UPDATE_HOLD_SECONDS - 0.1)
|
||||
monkeypatch.setattr(BigButton, "_handle_mouse_release", lambda *_args: None)
|
||||
monkeypatch.setattr(software, "system_time_valid", lambda: True)
|
||||
monkeypatch.setattr(software, "_request_update_download", lambda: downloads.append(True))
|
||||
monkeypatch.setattr(button, "_show_fast_update_confirmation", lambda: confirmations.append(True))
|
||||
|
||||
class ImmediateThread:
|
||||
def __init__(self, target, daemon):
|
||||
self.target = target
|
||||
|
||||
def start(self):
|
||||
self.target()
|
||||
|
||||
monkeypatch.setattr(software.threading, "Thread", ImmediateThread)
|
||||
button.set_enabled = lambda *_args: None
|
||||
button.set_icon = lambda *_args: None
|
||||
button._state = software.UpdaterState.IDLE
|
||||
button._txt_update_icon = None
|
||||
|
||||
button._handle_mouse_release(SimpleNamespace())
|
||||
|
||||
assert downloads == [True]
|
||||
assert confirmations == []
|
||||
|
||||
|
||||
def test_fast_update_worker_uses_galaxy_endpoint(monkeypatch):
|
||||
button = _make_button()
|
||||
requests = []
|
||||
responses = [
|
||||
{"message": "started"},
|
||||
{
|
||||
"running": True,
|
||||
"stage": "updating",
|
||||
"progressDetail": "Fetching latest shallow commit...",
|
||||
"lastError": "",
|
||||
},
|
||||
{
|
||||
"running": False,
|
||||
"stage": "rebooting",
|
||||
"progressDetail": "Update complete. Please wait for device to reboot.",
|
||||
"lastError": "",
|
||||
},
|
||||
]
|
||||
|
||||
def request(path="", method="GET"):
|
||||
requests.append((path, method))
|
||||
return responses.pop(0)
|
||||
|
||||
monkeypatch.setattr(software, "_galaxy_fast_update_request", request)
|
||||
monkeypatch.setattr(software.time, "sleep", lambda *_args: None)
|
||||
|
||||
button._run_fast_update()
|
||||
|
||||
assert requests == [("", "POST"), ("/status", "GET"), ("/status", "GET")]
|
||||
assert button._get_fast_update_state().stage == "rebooting"
|
||||
@@ -158,7 +158,7 @@ class HudRenderer(Widget):
|
||||
self._txt_exclamation_point: rl.Texture = gui_app.texture('icons_mici/exclamation_point.png', 44, 44)
|
||||
self._txt_egpu_loading: rl.Texture = gui_app.texture('icons_mici/egpu_loading.png', 60, 44)
|
||||
self._txt_egpu_green: rl.Texture = gui_app.texture('icons_mici/egpu_green.png', 60, 44)
|
||||
self._txt_egpu_orange: rl.Texture = gui_app.texture('icons_mici/egpu_orange.png', 60, 44)
|
||||
self._txt_egpu_orange: rl.Texture = gui_app.texture('icons_mici/egpu_orange.png', 75, 44)
|
||||
self._txt_egpu_crossed: rl.Texture = gui_app.texture('icons_mici/egpu_crossed.png', 60, 52)
|
||||
self._egpu_icon: rl.Texture | None = None
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from cereal import messaging, car
|
||||
from dataclasses import dataclass, field
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.selfdrive.controls.lib.lane_centering import get_lane_centering_visual_direction
|
||||
from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT
|
||||
from openpilot.selfdrive.ui.lib.starpilot_theme import get_param_color, get_theme_color, get_visual_color, is_stock_color_scheme, with_alpha
|
||||
from openpilot.selfdrive.ui.onroad.starpilot.rainbow_path import RainbowPath
|
||||
@@ -361,7 +362,31 @@ class ModelRenderer(Widget):
|
||||
|
||||
return LeadVehicle(glow=glow, chevron=chevron, fill_alpha=int(fill_alpha))
|
||||
|
||||
def _lane_line_palette(self) -> tuple[bool, rl.Color, rl.Color, bool]:
|
||||
def _lane_centering_direction(self) -> int:
|
||||
toggles = ui_state.starpilot_toggles
|
||||
sm = ui_state.sm
|
||||
if not sm.valid.get("modelV2", False) or not sm.valid.get("carState", False):
|
||||
return 0
|
||||
|
||||
car_state = sm["carState"]
|
||||
applied_correction = None
|
||||
if sm.valid.get("controlsState", False):
|
||||
try:
|
||||
applied_correction = sm["controlsState"].desiredCurvature - sm["modelV2"].action.desiredCurvature
|
||||
except (AttributeError, TypeError, ValueError):
|
||||
pass
|
||||
return get_lane_centering_visual_direction(
|
||||
sm["modelV2"], car_state.vEgo,
|
||||
toggles.get("lane_center_offset", 0.0),
|
||||
toggles.get("lane_centering_e2e_authority", 1.0),
|
||||
bool(toggles.get("lane_centering", False)),
|
||||
ui_state.status == UIStatus.ENGAGED or ui_state.always_on_lateral_active,
|
||||
bool(toggles.get("lane_centering_pause_on_signal", True)),
|
||||
bool(car_state.leftBlinker or car_state.rightBlinker),
|
||||
applied_correction,
|
||||
)
|
||||
|
||||
def _lane_line_palette(self) -> tuple[bool, rl.Color, rl.Color, int]:
|
||||
stock_scheme = is_stock_color_scheme(self._params)
|
||||
line_status = UIStatus.ENGAGED if ui_state.status == UIStatus.DISENGAGED and ui_state.always_on_lateral_active else ui_state.status
|
||||
|
||||
@@ -374,15 +399,15 @@ class ModelRenderer(Widget):
|
||||
if lane_color is None:
|
||||
lane_color = STOCK_LANE_LINES_COLOR if stock_scheme else get_theme_color("LaneLines", STOCK_LANE_LINES_COLOR)
|
||||
|
||||
lane_centering_active = bool(ui_state.starpilot_toggles.get("lane_centering", False)) and (
|
||||
ui_state.status == UIStatus.ENGAGED or ui_state.always_on_lateral_active
|
||||
)
|
||||
return stock_scheme, edge_color, lane_color, lane_centering_active
|
||||
lane_centering_direction = self._lane_centering_direction()
|
||||
return stock_scheme, edge_color, lane_color, lane_centering_direction
|
||||
|
||||
def _get_ll_color(self, prob: float, adjacent: bool, left: bool, stock_scheme: bool,
|
||||
edge_color: rl.Color, lane_color: rl.Color, lane_centering_active: bool = False):
|
||||
edge_color: rl.Color, lane_color: rl.Color, lane_centering_direction: int = 0):
|
||||
alpha = np.clip(prob, 0.0, 0.7)
|
||||
if lane_centering_active:
|
||||
lane_centering_line = adjacent and ((lane_centering_direction > 0 and not left) or
|
||||
(lane_centering_direction < 0 and left))
|
||||
if lane_centering_line:
|
||||
color = rl.Color(OCEAN_BLUE_LANE_LINES_COLOR.r, OCEAN_BLUE_LANE_LINES_COLOR.g,
|
||||
OCEAN_BLUE_LANE_LINES_COLOR.b, int(alpha * OCEAN_BLUE_LANE_LINES_COLOR.a))
|
||||
elif adjacent:
|
||||
@@ -408,13 +433,13 @@ class ModelRenderer(Widget):
|
||||
def _draw_lane_lines(self):
|
||||
"""Draw lane lines and road edges"""
|
||||
"""Two closest lines should be green (lane line or road edges)"""
|
||||
stock_scheme, edge_color, lane_color, lane_centering_active = self._lane_line_palette()
|
||||
stock_scheme, edge_color, lane_color, lane_centering_direction = self._lane_line_palette()
|
||||
for i, lane_line in enumerate(self._lane_lines):
|
||||
if lane_line.projected_points.size == 0:
|
||||
continue
|
||||
|
||||
color = self._get_ll_color(float(self._lane_line_probs[i]), i in (1, 2), i in (0, 1),
|
||||
stock_scheme, edge_color, lane_color, lane_centering_active)
|
||||
stock_scheme, edge_color, lane_color, lane_centering_direction)
|
||||
draw_polygon(self._rect, lane_line.projected_points, color)
|
||||
|
||||
for i, road_edge in enumerate(self._road_edges):
|
||||
|
||||
@@ -5,6 +5,7 @@ from cereal import messaging, car
|
||||
from dataclasses import dataclass, field
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.selfdrive.controls.lib.lane_centering import get_lane_centering_visual_direction
|
||||
from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT
|
||||
from openpilot.selfdrive.ui.lib.starpilot_theme import get_param_color, get_theme_color, get_visual_color, is_stock_color_scheme, with_alpha
|
||||
from openpilot.selfdrive.ui.onroad.radar_tracks import project_radar_points
|
||||
@@ -369,15 +370,35 @@ class ModelRenderer(Widget):
|
||||
|
||||
return LeadVehicle(glow=glow, chevron=chevron, fill_alpha=int(fill_alpha))
|
||||
|
||||
def _lane_centering_direction(self) -> int:
|
||||
toggles = ui_state.starpilot_toggles
|
||||
sm = ui_state.sm
|
||||
if not sm.valid.get("modelV2", False) or not sm.valid.get("carState", False):
|
||||
return 0
|
||||
|
||||
car_state = sm["carState"]
|
||||
applied_correction = None
|
||||
if sm.valid.get("controlsState", False):
|
||||
try:
|
||||
applied_correction = sm["controlsState"].desiredCurvature - sm["modelV2"].action.desiredCurvature
|
||||
except (AttributeError, TypeError, ValueError):
|
||||
pass
|
||||
return get_lane_centering_visual_direction(
|
||||
sm["modelV2"], car_state.vEgo,
|
||||
toggles.get("lane_center_offset", 0.0),
|
||||
toggles.get("lane_centering_e2e_authority", 1.0),
|
||||
bool(toggles.get("lane_centering", False)),
|
||||
ui_state.status == UIStatus.ENGAGED or ui_state.always_on_lateral_active,
|
||||
bool(toggles.get("lane_centering_pause_on_signal", True)),
|
||||
bool(car_state.leftBlinker or car_state.rightBlinker),
|
||||
applied_correction,
|
||||
)
|
||||
|
||||
def _draw_lane_lines(self):
|
||||
"""Draw lane lines and road edges"""
|
||||
lane_centering_active = bool(ui_state.starpilot_toggles.get("lane_centering", False)) and (
|
||||
ui_state.status == UIStatus.ENGAGED or ui_state.always_on_lateral_active
|
||||
)
|
||||
lane_centering_direction = self._lane_centering_direction()
|
||||
lane_lines_override = get_param_color(self._params, "LaneLinesColor", STOCK_LANE_LINES_COLOR.a)
|
||||
if lane_centering_active:
|
||||
lane_lines_color = OCEAN_BLUE_LANE_LINES_COLOR
|
||||
elif lane_lines_override is not None:
|
||||
if lane_lines_override is not None:
|
||||
lane_lines_color = lane_lines_override
|
||||
elif is_stock_color_scheme(self._params):
|
||||
lane_lines_color = STOCK_LANE_LINES_COLOR
|
||||
@@ -389,7 +410,10 @@ class ModelRenderer(Widget):
|
||||
continue
|
||||
|
||||
alpha = np.clip(self._lane_line_probs[i], 0.0, 0.7)
|
||||
color = with_alpha(lane_lines_color, int(alpha * lane_lines_color.a))
|
||||
lane_centering_line = (lane_centering_direction > 0 and i == 2) or \
|
||||
(lane_centering_direction < 0 and i == 1)
|
||||
line_color = OCEAN_BLUE_LANE_LINES_COLOR if lane_centering_line else lane_lines_color
|
||||
color = with_alpha(line_color, int(alpha * line_color.a))
|
||||
draw_polygon(self._rect, lane_line.projected_points, color)
|
||||
|
||||
for i, road_edge in enumerate(self._road_edges):
|
||||
|
||||
@@ -3,8 +3,6 @@ from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_planner import get_max_accel
|
||||
|
||||
ACCELERATION_PROFILES = {
|
||||
"STANDARD": 0,
|
||||
"ECO": 1,
|
||||
|
||||
@@ -19,7 +19,9 @@ CSC_MIN_SPEED = CITY_SPEED_LIMIT * CV.MPH_TO_MS
|
||||
CSC_CURVE_RELEASE_HOLD_TIME = 0.75
|
||||
OVERRIDE_FORCE_STOP_TIMER = 10
|
||||
STANDSTILL_FORCE_STOP_CLEAR_TIME = 0.75
|
||||
STANDSTILL_FORCE_STOP_LIGHT_HOLD_TIME = 5.0
|
||||
# Open-loop — green is undetectable at standstill, so this only needs to cover the
|
||||
# handoff to CEM+model ownership. Extra seconds are pure departure lag.
|
||||
STANDSTILL_FORCE_STOP_LIGHT_HOLD_TIME = 2.0
|
||||
FORCE_STOP_LIGHT_CLEAR_TIME = 0.5
|
||||
SLC_LEAD_DROP_RELAXATION_MIN_SPEED = 20.0 * CV.MPH_TO_MS
|
||||
SLC_LEAD_DROP_RELAXATION_MIN_DISTANCE = 30.0
|
||||
@@ -57,6 +59,8 @@ LEAD_VETO_M_OVERRIDES = {
|
||||
}
|
||||
FORCE_STOP_APPROACH_DECEL = 0.65 # m/s^2 — speed ceiling before commit. LOWER = more early
|
||||
# braking; don't go under FORCE_STOP_MODEL_APPROACH_DECEL
|
||||
# approachStopLength is published RAW: model_length converges from above, so rate-limiting
|
||||
# it inward freezes it far out and the constraint never binds. Tried, measured, don't re-add.
|
||||
ADAS_MAX_MS = 17.88 # 40 mph — cross-street ADAS guard
|
||||
DASH_SEED_M = 27.0 # ~88 ft — typical ADAS detection distance, used to snap
|
||||
# tracked length closer when dashboard confirms a sign
|
||||
@@ -74,6 +78,14 @@ FORCE_STOP_TURN_VETO_STEERING_ANGLE = 25.0
|
||||
FORCE_STOP_CURVE_VETO_MAX_ROAD_CURVATURE = 0.003
|
||||
FORCE_STOP_TURN_VETO_STOP_SEEN_HOLD_TIME = 4.0
|
||||
FORCE_STOP_DISTANCE_REANCHOR_MIN_GAP = 3.0 # m — ignore small model-horizon noise
|
||||
FORCE_STOP_REANCHOR_MIN_M = 40.0 # m — inside this only ratchet down; shouldStop doesn't
|
||||
# assert until ~10 m, so horizon jitter would release the stop
|
||||
FORCE_STOP_CAP_SLACK_M = 15.0 # m — the line can't move away, so tracked can never exceed
|
||||
# what it was at commit minus distance driven. Slack covers an
|
||||
# under-read at commit; without it that would stop us short.
|
||||
FORCE_STOP_CAP_TAPER_M = 60.0 # m — slack fades to 0 as the cap closes. The solver aims at
|
||||
# tracked, so slack held near the line is braking for a stop bar
|
||||
# that far past the real one.
|
||||
|
||||
# Knob bounds (mirror of UI slider; defense in depth)
|
||||
OFFSET_FT_MIN = -20
|
||||
@@ -177,9 +189,11 @@ class StarPilotVCruise:
|
||||
self.force_stop_from_light = False
|
||||
self.force_stop_light_clear_since = None
|
||||
self.controls_enabled_previously = False
|
||||
self.approach_stop_length = 0.0 # published as starpilotPlan.approachStopLength
|
||||
# Kinematic distance estimator. Same attribute also published as
|
||||
# starpilotPlan.forcingStopLength, so the existing reader keeps working.
|
||||
self.tracked_model_length = 0.0
|
||||
self.force_stop_distance_cap = 0.0 # odometry ceiling, re-seeded until commit
|
||||
|
||||
self.stop_sign_confirmed = False
|
||||
self.stop_seen_on_approach_at = None
|
||||
@@ -603,6 +617,9 @@ class StarPilotVCruise:
|
||||
offset_ft = max(OFFSET_FT_MIN, min(OFFSET_FT_MAX, offset_ft_raw))
|
||||
offset_m = offset_ft * FT_TO_M
|
||||
|
||||
# cleared on every path; only the far-approach envelope below republishes it
|
||||
self.approach_stop_length = 0.0
|
||||
|
||||
if force_standstill_enabled and not self.override_force_standstill:
|
||||
self.forcing_stop = True
|
||||
self.tracked_model_length = 0.0
|
||||
@@ -630,7 +647,7 @@ class StarPilotVCruise:
|
||||
model_wants_stop = False
|
||||
if (
|
||||
not dash_active and
|
||||
self.tracked_model_length > force_stop_handoff_m and
|
||||
self.tracked_model_length > max(force_stop_handoff_m, FORCE_STOP_REANCHOR_MIN_M) and
|
||||
not model_wants_stop and
|
||||
model_length > self.tracked_model_length + FORCE_STOP_DISTANCE_REANCHOR_MIN_GAP and
|
||||
(
|
||||
@@ -642,6 +659,12 @@ class StarPilotVCruise:
|
||||
self.tracked_model_length = model_length
|
||||
else:
|
||||
self.tracked_model_length = min(self.tracked_model_length, model_length)
|
||||
# Odometry ceiling: the line can't recede, so a re-anchor may never exceed what we
|
||||
# had at commit minus what we've driven. Bounds a ballooning horizon (seen +95 m)
|
||||
# that the REANCHOR_MIN floor can't catch, since that floor trusts the estimate.
|
||||
self.force_stop_distance_cap = max(self.force_stop_distance_cap - (v_ego * DT_MDL), 0.0)
|
||||
cap_slack = FORCE_STOP_CAP_SLACK_M * min(self.force_stop_distance_cap / FORCE_STOP_CAP_TAPER_M, 1.0)
|
||||
self.tracked_model_length = min(self.tracked_model_length, self.force_stop_distance_cap + cap_slack)
|
||||
if dash_active:
|
||||
if model_length < DASH_MODEL_AGREE_M:
|
||||
self.tracked_model_length = min(self.tracked_model_length, DASH_SEED_M)
|
||||
@@ -675,6 +698,7 @@ class StarPilotVCruise:
|
||||
self.stop_sign_confirmed = False
|
||||
|
||||
self.tracked_model_length = self.starpilot_planner.model_length
|
||||
self.force_stop_distance_cap = self.tracked_model_length
|
||||
|
||||
targets = [v_cruise]
|
||||
if self.csc_target >= CSC_MIN_SPEED:
|
||||
@@ -720,6 +744,8 @@ class StarPilotVCruise:
|
||||
adjacent_stop_d = self._get_adjacent_stop_distance(sm)
|
||||
if adjacent_stop_d is not None:
|
||||
approach_d = min(approach_d, adjacent_stop_d)
|
||||
# pre-offset, so it hands off to forcingStopLength at commit without a step
|
||||
self.approach_stop_length = max(approach_d, 0.0)
|
||||
approach_d += offset_m + force_stop_distance_bias_m
|
||||
if approach_d > force_stop_handoff_m:
|
||||
targets.append(math.sqrt(2.0 * FORCE_STOP_APPROACH_DECEL * (approach_d - force_stop_handoff_m)))
|
||||
|
||||
@@ -31,7 +31,10 @@ from openpilot.starpilot.controls.lib.starpilot_vcruise import StarPilotVCruise
|
||||
from openpilot.starpilot.controls.lib.weather_checker import WeatherChecker
|
||||
|
||||
RADARLESS_TRACK_HOLD_TIME = 0.45
|
||||
FORCE_STOP_JERK_SCALE = 0.32 # accel-change cost multiplier while forcing_stop (125 -> ~40)
|
||||
FORCE_STOP_JERK_SCALE = 0.20 # accel-change cost multiplier for the whole stop approach,
|
||||
# envelope included (125 -> 25). Lower = reaches the braking
|
||||
# target sooner; it does not make the target deeper. Response
|
||||
# is super-linear here, so raise it if onset feels like a step.
|
||||
FORCE_STOP_JERK_SCALE_OVERRIDES = {
|
||||
# The Elantra's current force-stop ramp is smooth, but it waits too long
|
||||
# before building decel and then arrives at the initial brake too abruptly.
|
||||
@@ -308,7 +311,9 @@ class StarPilotPlanner:
|
||||
except (KeyError, IndexError, TypeError, AttributeError):
|
||||
car_params = None
|
||||
|
||||
if self.starpilot_vcruise.forcing_stop:
|
||||
# Also while the far-approach envelope is running: at onset the ramp reaches only
|
||||
# ~-0.5 m/s^2 after a second, so the first seconds of a detected red are mostly lost.
|
||||
if self.starpilot_vcruise.forcing_stop or self.starpilot_vcruise.approach_stop_length > 0.0:
|
||||
jerk_scale = get_force_stop_jerk_scale(car_params)
|
||||
elif self.tracking_lead:
|
||||
# Elantra vision leads can hand off from cruise to lead0 while closing
|
||||
@@ -346,6 +351,7 @@ class StarPilotPlanner:
|
||||
|
||||
starpilotPlan.forcingStop = self.starpilot_vcruise.forcing_stop
|
||||
starpilotPlan.forcingStopLength = self.starpilot_vcruise.tracked_model_length
|
||||
starpilotPlan.approachStopLength = float(self.starpilot_vcruise.approach_stop_length)
|
||||
starpilotPlan.stopSignConfirmed = self.starpilot_vcruise.stop_sign_confirmed
|
||||
|
||||
starpilotPlan.starpilotEvents = self.starpilot_events.events.to_msg()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
from typing import cast
|
||||
import os, ctypes, struct, hashlib, functools, importlib, mmap, errno, array, contextlib, sys, weakref, itertools, collections, atexit
|
||||
import os, ctypes, struct, hashlib, functools, importlib, mmap, errno, array, contextlib, sys, weakref, itertools, collections, atexit, time
|
||||
assert sys.platform != 'win32'
|
||||
from dataclasses import dataclass
|
||||
from tinygrad.runtime.support.hcq import HCQCompiled, HCQAllocator, HCQBuffer, HWQueue, CLikeArgsState, HCQSignal, HCQProgram, FileIOInterface
|
||||
@@ -25,6 +25,7 @@ SQTT = ContextVar("SQTT", abs(VIZ.value)>=2)
|
||||
SQTT_ITRACE_SE_MASK, SQTT_LIMIT_SE, SQTT_SIMD_SEL, SQTT_TOKEN_EXCLUDE = \
|
||||
ContextVar("SQTT_ITRACE_SE_MASK", 0b11), ContextVar("SQTT_LIMIT_SE", 0), ContextVar("SQTT_SIMD_SEL", 0), ContextVar("SQTT_TOKEN_EXCLUDE", 0)
|
||||
PMC = ContextVar("PMC", abs(VIZ.value)>=2)
|
||||
AMD_USB_POLL_US = getenv("AMD_USB_POLL_US", 500) # microseconds to sleep between USB signal polls. 0 disables
|
||||
EVENT_INDEX_PARTIAL_FLUSH = 4 # based on a comment in nvd.h
|
||||
WAIT_REG_MEM_FUNCTION_EQ = 3 # ==
|
||||
WAIT_REG_MEM_FUNCTION_NEQ = 4 # !=
|
||||
@@ -45,8 +46,9 @@ class AMDSignal(HCQSignal):
|
||||
def __init__(self, *args, **kwargs): super().__init__(*args, **{**kwargs, 'timestamp_divider': 100})
|
||||
|
||||
def _sleep(self, time_spent_since_last_sleep_ms:int):
|
||||
# Reasonable to sleep for long workloads (which take more than 200ms) and only timeline signals.
|
||||
if time_spent_since_last_sleep_ms > 200 and self.owner is not None: self.owner.iface.sleep(200)
|
||||
# USB signals live in VRAM across the link, so yield between polls. Native AMD only blocks after 200 ms.
|
||||
if self.owner is not None and self.owner.is_usb() and AMD_USB_POLL_US: time.sleep(AMD_USB_POLL_US / 1e6)
|
||||
elif time_spent_since_last_sleep_ms > 200 and self.owner is not None: self.owner.iface.sleep(200)
|
||||
|
||||
class AMDComputeQueue(HWQueue):
|
||||
def __init__(self, dev:AMDDevice):
|
||||
|
||||
Reference in New Issue
Block a user