Files
StarPilot/openpilot/starpilot/longitudinal/force_stop.py
T
firestar5683 e4f3d6661c Longitudinal
Implement acceleration profiles, FollowJerk, force-stop and lead-departure owners. Keep selected acceleration limits and hazard arbitration explicit at their shared boundaries.

Co-authored-by: whoisdomi <dcantu83@gmail.com>
2026-10-14 20:02:39 -05:00

216 lines
8.8 KiB
Python

"""Force Stop's model approach, commitment, distance and driver-release policy."""
from dataclasses import dataclass
import math
from openpilot.common.realtime import DT_MDL
@dataclass(frozen=True)
class StopTune:
handoff_m: float = 6.0
distance_bias_m: float = 0.0
lead_veto_m: float = 75.0
jerk_scale: float = 0.32
reanchor_speed_tolerance: float | None = None
low_speed_hold_mps: float | None = None
def vehicle_tune(fingerprint: str) -> StopTune:
if fingerprint == 'TOYOTA_CAMRY_TSS2':
return StopTune(handoff_m=4.5, distance_bias_m=6.0)
if fingerprint == 'HYUNDAI_ELANTRA_2021':
return StopTune(lead_veto_m=90.0, jerk_scale=0.8)
if fingerprint == 'HYUNDAI_SANTA_FE_2022':
return StopTune(reanchor_speed_tolerance=0.25, low_speed_hold_mps=2.5)
return StopTune()
@dataclass(frozen=True)
class StopFrame:
model_ns: int
speed_mps: float
horizon_m: float | None
light: bool
raw_model_stopped: bool
model_should_stop: bool
standstill: bool
enabled: bool
pedal_override: bool
tracking_lead: bool
lead_present: bool
lead_distance_m: float
lead_speed_mps: float
driving_in_curve: bool
road_curvature: float | None
blinker: bool
steering_angle_deg: float
resume_requested: bool = False
perception_available: bool = True
@dataclass(frozen=True)
class StopPlan:
model_ns: int = 0
speed_ceiling_mps: float | None = None
obstacle_m: float | None = None
forcing: bool = False
should_stop: bool = False
tracked_distance_m: float = 0.0
approach_distance_m: float = 0.0
jerk_scale: float = 1.0
manual_hold: bool = False
takeoff_light_observed: bool = False
takeoff_light: bool = False
class ForceStop:
def __init__(self):
self.reset()
def reset(self):
self.last_model_ns = 0
self.timer = 0.0
self.forcing = False
self.from_light = False
self.light_clear_ns = 0
self.override_until_ns = 0
self.entry_speed = None
self.activation_gate = False
self.stop_seen_ns = 0
self.standstill_since_ns = 0
self.standstill_reason = None
self.previously_enabled = False
self.distance = 0.0
self.distance_cap = 0.0
def _clear_standstill(self):
self.standstill_since_ns = 0
self.standstill_reason = None
def step(self, frame: StopFrame, tune: StopTune, offset_ft: int = 0) -> StopPlan:
now = frame.model_ns
if (type(now) is not int or now <= 0 or now <= self.last_model_ns or
self.last_model_ns and now - self.last_model_ns > 250_000_000):
self.reset()
return StopPlan()
dt = (now - self.last_model_ns) / 1e9 if self.last_model_ns else DT_MDL
self.last_model_ns = now
if not frame.enabled:
self.reset()
return StopPlan(model_ns=now)
speed = frame.speed_mps
horizon = frame.horizon_m if frame.horizon_m is not None else math.nan
curvature = frame.road_curvature if frame.road_curvature is not None else math.nan
if (not all(math.isfinite(value) for value in (speed, frame.steering_angle_deg)) or
frame.perception_available and not all(math.isfinite(value) for value in (horizon, curvature))):
self.reset()
return StopPlan()
available = frame.perception_available
if available and (frame.light or frame.raw_model_stopped):
if not frame.standstill:
self.stop_seen_ns = now
if frame.standstill:
self.stop_seen_ns = 0
stop_then_turn = bool(self.stop_seen_ns and now - self.stop_seen_ns < 4_000_000_000)
turn = speed <= 18 * 0.44704 and frame.blinker and abs(frame.steering_angle_deg) >= 25 and not stop_then_turn
curved = available and abs(curvature) >= .003 and not stop_then_turn
lead = (frame.lead_present and frame.lead_distance_m < tune.lead_veto_m and frame.lead_speed_mps < speed + 2)
model_active = available and horizon < (108.0 if self.activation_gate and frame.light else 100.0)
self.activation_gate = model_active and frame.light
permitted = (available and now >= self.override_until_ns and not frame.driving_in_curve and not turn and
not frame.tracking_lead and not lead)
model_path = frame.light and model_active and permitted and not curved
active = model_path
if model_path:
self.from_light = True
self.light_clear_ns = 0
elif not self.forcing:
self.from_light = False
self.light_clear_ns = 0
stop_scene = available and (active or frame.raw_model_stopped)
# Reopened geometry may withdraw a moving approach; it cannot classify
# a stationary hold as a green light.
go_scene = available and horizon >= 50.0 and not frame.raw_model_stopped and not frame.model_should_stop
engaged_stopped = not self.previously_enabled and frame.standstill
if frame.standstill and (self.forcing or engaged_stopped and stop_scene) and self.standstill_reason is None:
self.standstill_since_ns = now
self.standstill_reason = 'unclassified'
self.distance = 0
driver_release = frame.pedal_override or frame.resume_requested
if self.standstill_reason is not None and driver_release:
self.override_until_ns = now + 10_000_000_000
self._clear_standstill()
held = self.standstill_reason is not None
if not available and not self.forcing:
self.timer = 0
elif active and not frame.standstill:
self.timer = min(2.0, self.timer + DT_MDL)
elif turn and not self.forcing and not frame.standstill:
self.timer = 0
elif held:
self.timer = max(self.timer, .5)
elif self.forcing and frame.standstill and not frame.light and not frame.raw_model_stopped:
self.timer = 0
else:
self.timer = max(0, self.timer - DT_MDL * .25)
force = self.timer >= .5 or self.forcing and not frame.standstill or held
light_cleared = (not held and self.forcing and self.from_light and not frame.standstill and
available and not frame.light and go_scene)
low_speed_hold = (light_cleared and tune.low_speed_hold_mps is not None and self.entry_speed is not None and
speed <= tune.low_speed_hold_mps and speed < self.entry_speed - .25)
if light_cleared and not low_speed_hold:
if not self.light_clear_ns:
self.light_clear_ns = now
elif now - self.light_clear_ns >= 500_000_000:
self.forcing = self.from_light = force = False
self.timer = 0
self.light_clear_ns = 0
else:
self.light_clear_ns = 0
if self.forcing and frame.standstill and not force:
self.override_until_ns = now + 10_000_000_000
if driver_release and force:
self.override_until_ns = now + 10_000_000_000
overridden = now < self.override_until_ns
offset = max(-20, min(20, offset_ft)) * .3048
approach = 0.0
ceiling = None
if force and not overridden:
if self.entry_speed is None and not frame.standstill:
self.entry_speed = speed
self.forcing |= not frame.standstill or held
if held:
self.distance = 0
ceiling = 0.0
else:
self.distance = max(0.0, self.distance - speed * dt)
if (available and self.distance > max(tune.handoff_m, 40) and not frame.model_should_stop and
horizon > self.distance + 3 and (tune.reanchor_speed_tolerance is None or self.entry_speed is None or
speed >= self.entry_speed - tune.reanchor_speed_tolerance)):
self.distance = horizon
elif available:
self.distance = min(self.distance, horizon)
self.distance_cap = max(0.0, self.distance_cap - speed * dt)
self.distance = min(self.distance, self.distance_cap + 15 * min(self.distance_cap / 60, 1))
effective = self.distance + offset + tune.distance_bias_m
ceiling = math.sqrt(2 * .65 * max(0, effective - tune.handoff_m))
else:
self.forcing = False
self.entry_speed = None
self._clear_standstill()
self.distance = self.distance_cap = horizon if available else 0.0
if frame.light and permitted and not curved and not overridden:
approach = horizon
effective = approach + offset + tune.distance_bias_m
if effective > tune.handoff_m:
ceiling = math.sqrt(2 * .65 * (effective - tune.handoff_m))
self.previously_enabled = frame.enabled
stop_length = self.distance if self.forcing and self.distance > tune.handoff_m else approach
# The MPC obstacle includes its standing-distance term; adding it at the
# native boundary avoids moving the actual stop line by that buffer.
obstacle = stop_length * .93 + offset + tune.distance_bias_m if stop_length > tune.handoff_m else None
return StopPlan(now, ceiling, obstacle, self.forcing, bool(self.forcing and ceiling is not None and ceiling <= .5),
self.distance if self.forcing else 0.0, approach,
tune.jerk_scale if self.forcing or approach > 0 else 1.0, bool(held and self.forcing and not overridden))