Compare commits

...

10 Commits

Author SHA1 Message Date
whoisdomi fbcc0bed66 Force Stop: Fast Approach
Force Stop is now better at faster approach speeds. It has more authority on braking in general.

Force Stop Distance Offset: still the lever that moves the ending point sooner or later.

Radar bug fixed
2026-08-27 08:21:15 -05:00
firestarsdog 78aba03b4f Revert "tell me wtf is wrong"
This reverts commit 2c002755c7.
2026-08-27 03:19:39 -04:00
firestarsdog 2c002755c7 tell me wtf is wrong 2026-08-26 23:20:52 -04:00
firestar5683 81d20d304f gpu 2026-08-26 20:50:16 -05:00
firestar5683 d683da24ea lines 2026-08-26 16:28:11 -05:00
firestar5683 4509316877 model 2026-08-26 16:17:04 -05:00
whoisdomi 6a17743513 Force Stop Tweak
Ratched only down below 40m to prevent model jitter from overshooting.

Original PR author: whoisdomi <dcantu83@gmail.com>.
2026-08-26 10:24:29 -05:00
whoisdomi e20ea5d0d1 Remove Takeoff Twitch
Limit the curvature guard to a short pull-away window and reject invalid model plans.

Original PR author: whoisdomi <dcantu83@gmail.com>.
2026-08-26 10:24:25 -05:00
firestar5683 f20b256473 UI 2026-08-26 08:28:28 -05:00
whoisdomi a88da4071f Force Stop hold 2 sec 2026-08-26 08:12:05 -05:00
22 changed files with 754 additions and 81 deletions
+5
View File
@@ -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

+75 -11
View File
@@ -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:
+47 -4
View File
@@ -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
+16 -1
View File
@@ -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)
)
+15 -3
View File
@@ -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
+104 -1
View File
@@ -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
+7 -3
View File
@@ -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
View File
@@ -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
+10 -12
View File
@@ -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():
+137 -1
View File
@@ -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"
+1 -1
View File
@@ -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
+34 -9
View File
@@ -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):
+31 -7
View File
@@ -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):
-2
View File
@@ -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,
+28 -2
View File
@@ -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)))
+8 -2
View File
@@ -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()
+5 -3
View File
@@ -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):