Files
StarPilot/openpilot/starpilot/conditional_mode/chill_scene.py
T
firestar5683 d8708b65c7 CEM
Implement conditional driving modes with explicit scene, button and traffic state ownership.

Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com>
Co-authored-by: Harald Schäfer <harald.the.engineer@gmail.com>
Co-authored-by: whoisdomi <dcantu83@gmail.com>
2026-10-14 20:02:39 -05:00

182 lines
7.1 KiB
Python

"""Source-bound low-speed stop and launch-assist evidence for Conditional Chill.
This module only reports scene evidence. It never selects a driving mode or
reads a service, Params, or vehicle signal on its own.
"""
from __future__ import annotations
from dataclasses import dataclass
import math
from openpilot.common.realtime import DT_MDL
MPH_TO_MPS = 0.44704
LOW_SPEED_STOP_MAX_MPS = 18.0 * MPH_TO_MPS
LAUNCH_EXIT_MPS = 15.0 * MPH_TO_MPS
LAUNCH_ENTRY_MAX_MPS = 1.0
MAX_SOURCE_AGE_S = 0.25
MAX_FRAME_GAP_S = 2.0 * DT_MDL
@dataclass(frozen=True)
class ChillLead:
present: bool | None
distance_m: float | None = None
speed_mps: float | None = None
relative_speed_mps: float | None = None # Raw lead.vRel; never inferred from vLead - vEgo.
accel_mps2: float | None = None
@dataclass(frozen=True)
class ChillSceneFrame:
observed_mono_s: float
model_tick_mono_s: float
now_mono_s: float
speed_mps: float | None
lead: ChillLead | None
tracking_lead: bool | None
raw_model_stopped: bool | None
model_stopped: bool | None
stop_light_model_detected: bool | None
stop_light_detected: bool | None
stop_sign_confirmed: bool | None
forcing_stop: bool | None
red_light: bool | None
plan_forcing_stop: bool | None
should_stop: bool | None
allow_throttle: bool | None
selfdrive_enabled: bool | None
@dataclass(frozen=True)
class ChillSceneObservation:
low_speed_stop_scene: bool | None
launch_candidate: bool | None
forced_exit: bool
launch_status: str | None # "speed" or "lead" when candidate is true.
speed_reason: str | None
lead_reason: str | None
def _finite(value: object, low: float, high: float) -> float | None:
if isinstance(value, bool) or not isinstance(value, (int, float)):
return None
try:
number = float(value)
except (OverflowError, ValueError):
return None
return number if math.isfinite(number) and low <= number <= high else None
def _bool(value: object) -> bool | None:
return value if type(value) is bool else None
class ChillSceneDetector:
"""One model-tick launch latch with immediate frozen exit conditions."""
def __init__(self):
self.reset()
def reset(self) -> None:
self.launch_active = False
self.last_model_tick_mono_s: float | None = None
@staticmethod
def _low_speed_stop(frame: ChillSceneFrame, speed: float, lead: ChillLead | None) -> bool | None:
if speed >= LOW_SPEED_STOP_MAX_MPS:
return False
if frame.raw_model_stopped or frame.model_stopped or frame.stop_light_model_detected:
return True
lead_stop = None
if lead is not None and _bool(lead.present) is not None:
if not lead.present:
lead_stop = False
else:
distance = _finite(lead.distance_m, 0.0, 500.0)
lead_speed = _finite(lead.speed_mps, -30.0, 100.0)
if distance is not None and lead_speed is not None:
lead_stop = distance < max(40.0, speed * 4.5) and lead_speed < max(6.0, speed + 0.5)
if lead_stop:
return True
if (any(_bool(value) is None for value in (
frame.raw_model_stopped, frame.model_stopped, frame.stop_light_model_detected,
)) or lead_stop is None):
return None
return False
@staticmethod
def _eligible(frame: ChillSceneFrame, speed: float, lead: ChillLead, active: bool) -> tuple[bool, str, str]:
if speed > LAUNCH_ENTRY_MAX_MPS and not active:
return False, "above_entry_speed", "not_evaluated"
if not frame.selfdrive_enabled:
return False, "selfdrive_disabled", "not_evaluated"
if (frame.stop_light_detected or frame.stop_light_model_detected or frame.raw_model_stopped or
frame.model_stopped or frame.stop_sign_confirmed or frame.forcing_stop or frame.red_light or
frame.plan_forcing_stop):
return False, "stop_scene", "not_evaluated"
if frame.should_stop or not frame.allow_throttle:
return False, "longitudinal_stop", "not_evaluated"
if not lead.present and not frame.tracking_lead:
return True, "go", "no_lead"
assert lead.speed_mps is not None and lead.relative_speed_mps is not None and lead.accel_mps2 is not None
lead_delta = lead.speed_mps - speed
closing_speed = max(0.0, speed - lead.speed_mps)
departing = (
lead.speed_mps >= 0.35 and lead_delta >= 0.2 and lead.relative_speed_mps >= 0.2 and
lead.accel_mps2 >= 0.08 and max(0.0, -lead.accel_mps2) <= 0.2 and closing_speed <= 0.75
)
return departing, "go" if departing else "lead_not_departing", "departing" if departing else "not_departing"
def step(self, frame: ChillSceneFrame) -> ChillSceneObservation:
unknown = ChillSceneObservation(None, None, self.launch_active, None, "unavailable", "unavailable")
observed = _finite(frame.observed_mono_s, 0.0, 1e12)
tick = _finite(frame.model_tick_mono_s, 0.0, 1e12)
now = _finite(frame.now_mono_s, 0.0, 1e12)
speed = _finite(frame.speed_mps, 0.0, 80.0)
lead = frame.lead
if (observed is None or tick is None or now is None or
not 0.0 <= now - observed <= MAX_SOURCE_AGE_S or not observed <= tick <= now or speed is None):
self.reset()
return unknown
if self.last_model_tick_mono_s is not None:
elapsed = tick - self.last_model_tick_mono_s
if elapsed <= 0.0 or elapsed > MAX_FRAME_GAP_S:
self.reset()
return unknown
self.last_model_tick_mono_s = tick
low_speed_stop = self._low_speed_stop(frame, speed, lead)
launch_inputs = (
frame.raw_model_stopped, frame.model_stopped,
frame.stop_light_model_detected, frame.stop_light_detected,
frame.stop_sign_confirmed, frame.forcing_stop, frame.red_light,
frame.plan_forcing_stop, frame.should_stop, frame.allow_throttle,
frame.selfdrive_enabled,
)
if (lead is None or _bool(lead.present) is None or
(not lead.present and _bool(frame.tracking_lead) is None) or
any(_bool(value) is None for value in launch_inputs)):
forced_exit = self.launch_active
self.launch_active = False
return ChillSceneObservation(low_speed_stop, None, forced_exit, None, "unavailable", "unavailable")
if lead.present or frame.tracking_lead:
if (_finite(lead.distance_m, 0.0, 500.0) is None or
_finite(lead.speed_mps, -30.0, 100.0) is None or
_finite(lead.relative_speed_mps, -100.0, 100.0) is None or
_finite(lead.accel_mps2, -20.0, 20.0) is None):
forced_exit = self.launch_active
self.launch_active = False
return ChillSceneObservation(low_speed_stop, None, forced_exit, None, "unavailable", "unavailable")
if self.launch_active and speed >= LAUNCH_EXIT_MPS:
self.launch_active = False
return ChillSceneObservation(low_speed_stop, False, True, None, "exit_speed", "not_evaluated")
eligible, speed_reason, lead_reason = self._eligible(frame, speed, lead, self.launch_active)
if self.launch_active and not eligible:
self.launch_active = False
return ChillSceneObservation(low_speed_stop, False, True, None, speed_reason, lead_reason)
self.launch_active = eligible
status = ("lead" if lead.present or frame.tracking_lead else "speed") if eligible else None
return ChillSceneObservation(low_speed_stop, eligible, False, status, speed_reason, lead_reason)