Files
StarPilot/openpilot/starpilot/longitudinal/experimental_release.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

114 lines
4.8 KiB
Python

"""Optional moving-lead acceleration restraint after leaving Experimental mode."""
import math
from dataclasses import dataclass
ConditionalHandoff = tuple[str, int, int, str]
MAX_SOURCE_AGE_NS = 250_000_000
MAX_FRAME_GAP_NS = 150_000_000
HOLD_NS = 750_000_000
LOW_SPEED_HOLD_NS = 3_000_000_000
LOW_SPEED_MPS = 12.0
MIN_DELTA_A = 0.12
ACCEL_STEP = 0.06
SOURCES = ('modelV2', 'carState', 'radarState', 'carControl', 'controlsState', 'selfdriveState')
@dataclass(frozen=True)
class ReleaseLead:
distance: float
speed: float
acceleration: float
radar: bool
probability: float
lateral_offset: float
@dataclass(frozen=True)
class ReleaseFrame:
now_ns: int
model_ns: int
experimental: bool
speed: float
lead: ReleaseLead | None
def project(sm, cp, now_ns: int, *, lead_index: int | None,
conditional_handoff: ConditionalHandoff | None) -> ReleaseFrame | None:
try:
if not valid_key(conditional_handoff) or conditional_handoff is None or type(now_ns) is not int or now_ns <= 0:
return None
drive_id = conditional_handoff[2]
for name in SOURCES:
stamp = sm.logMonoTime[name]
if (sm.valid[name] is not True or sm.alive[name] is not True or type(stamp) is not int or
not drive_id <= stamp <= now_ns or now_ns - stamp > MAX_SOURCE_AGE_NS):
return None
car = sm['carState']
if (not cp.openpilotLongitudinalControl or not sm['carControl'].longActive or
not sm['selfdriveState'].enabled or not car.canValid or car.canTimeout or
car.gasPressed or car.brakePressed or car.standstill or sm['controlsState'].forceDecel or
sm['modelV2'].action.shouldStop):
return None
leads = (sm['radarState'].leadOne, sm['radarState'].leadTwo)
selected = (leads[lead_index] if lead_index is not None else
next((lead for lead in leads if lead.present), None))
lead = None
if selected is not None and selected.present:
lead = ReleaseLead(float(selected.dRel), float(selected.vLead), float(selected.aLeadK),
bool(selected.radar), float(selected.modelProb), float(selected.yRel))
return ReleaseFrame(now_ns, sm.logMonoTime['modelV2'], bool(sm['selfdriveState'].experimentalMode),
float(car.vEgo), lead)
except (AttributeError, IndexError, KeyError, TypeError, ValueError, OverflowError):
return None
def valid_key(key: ConditionalHandoff | None) -> bool:
return (type(key) is tuple and len(key) == 4 and type(key[0]) is str and bool(key[0]) and
type(key[1]) is int and key[1] > 0 and type(key[2]) is int and key[2] > 0 and
key[3] in ('conditional_experimental', 'conditional_chill'))
class ExperimentalRelease:
def __init__(self):
self.reset()
def reset(self) -> None:
self.key: ConditionalHandoff | None = None
self.previous: ReleaseFrame | None = None
self.release_until_ns = 0
def step(self, key: ConditionalHandoff | None, frame: ReleaseFrame | None, *, previous_target: float,
target: float, follow_seconds: float, blocked: bool) -> float | None:
if (not valid_key(key) or frame is None or blocked or
not all(math.isfinite(value) for value in (previous_target, target, follow_seconds, frame.speed)) or
frame.speed < 0 or not 0.5 <= follow_seconds <= 3.0 or type(frame.experimental) is not bool or
type(frame.now_ns) is not int or type(frame.model_ns) is not int or
not 0 < frame.model_ns <= frame.now_ns or frame.now_ns - frame.model_ns > MAX_SOURCE_AGE_NS):
self.reset()
return None
lead = frame.lead
if lead is not None and not all(math.isfinite(value) for value in
(lead.distance, lead.speed, lead.acceleration, lead.probability, lead.lateral_offset)):
self.reset()
return None
if key != self.key:
self.reset()
previous = self.previous
if previous is not None and (not 0 < frame.model_ns - previous.model_ns <= MAX_FRAME_GAP_NS or
not 0 < frame.now_ns - previous.now_ns <= MAX_FRAME_GAP_NS):
self.reset()
return None
self.key = key
self.previous = frame
if frame.experimental:
self.release_until_ns = 0
elif previous is not None and previous.experimental:
self.release_until_ns = frame.now_ns + (LOW_SPEED_HOLD_NS if frame.speed < LOW_SPEED_MPS else HOLD_NS)
if (frame.now_ns >= self.release_until_ns or lead is None or frame.speed < 2.5 or lead.speed < 5.0 or
not -1.0 <= lead.speed - frame.speed <= 1.5 or max(0.0, -lead.acceleration) > 0.2 or
(not lead.radar and lead.probability < 0.9) or abs(lead.lateral_offset) > 1.5 or
lead.distance / max(frame.speed, 1e-3) < follow_seconds or target - previous_target < MIN_DELTA_A):
return None
return min(target, previous_target + ACCEL_STEP)