mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-12 15:33:45 +08:00
14cb60b8e5
Implement acceleration profiles, FollowJerk, force-stop and lead-departure owners. Keep selected acceleration limits and hazard arbitration explicit at their shared boundaries.
431 lines
20 KiB
Python
431 lines
20 KiB
Python
"""Validated saved longitudinal profiles resolved for the native planner."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
from dataclasses import astuple, dataclass
|
|
|
|
from openpilot.common.constants import CV
|
|
from opendbc.car.structs import car
|
|
from opendbc.car.interfaces import ACCEL_MIN, ACCEL_MAX
|
|
from openpilot.starpilot.longitudinal.accel_profile import A_CRUISE_MAX_VALS_TRAFFIC_ALL, interpolate_accel_profile
|
|
from openpilot.starpilot.longitudinal.profile_document import (
|
|
active_personality_id, interpolate_category_curve, is_truck_fingerprint,
|
|
)
|
|
from openpilot.starpilot.longitudinal.profile_preferences import (
|
|
FOLLOW_MIN_SECONDS, FOLLOW_MAX_SECONDS, NAMES, SCALARS, TRAFFIC_JERK_SUFFIXES,
|
|
read_document_value, read_master_value, read_profile_health, read_traffic_health,
|
|
)
|
|
|
|
JERK_MIN = 0.25
|
|
JERK_MAX = 2.0
|
|
LEGACY_FOLLOW_BP = (45.0 * CV.MPH_TO_MS, 70.0 * CV.MPH_TO_MS)
|
|
TRAFFIC_BP = (0.0, 25.0) # m/s, not mph.
|
|
TRAFFIC_CRUISE_BRAKE_MAGNITUDE = 1.2 * 0.35 # Magnitude of A_CRUISE_MIN * 0.35.
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ProfileSettings:
|
|
document: dict | None
|
|
profile_enabled: dict[str, bool]
|
|
follow: dict[str, tuple[float, float]]
|
|
jerk: dict[str, tuple[float, float, float, float, float]]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class TrafficSettings:
|
|
valid: bool
|
|
reason: str
|
|
document: dict | None
|
|
profile_enabled: bool
|
|
follow: tuple[float, float]
|
|
jerk: tuple[tuple[float, float], ...]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ProfileTuning:
|
|
personality_id: str
|
|
follow_seconds: float
|
|
acceleration_jerk: float
|
|
deceleration_jerk: float
|
|
speed_jerk: float
|
|
speed_decrease_jerk: float
|
|
danger_jerk: float
|
|
acceleration_max: float | None = None
|
|
cruise_brake_magnitude: float | None = None
|
|
slc_braking_style: str = 'standard'
|
|
traffic_braking_custom: bool = False
|
|
custom_acceleration: bool = False
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class AppliedProfile:
|
|
follow_seconds: float
|
|
acceleration_max: float
|
|
cruise_brake_magnitude: float
|
|
acceleration_jerk: float
|
|
deceleration_jerk: float
|
|
speed_jerk: float
|
|
speed_decrease_jerk: float
|
|
danger_jerk: float
|
|
|
|
|
|
def _move(current: float, target: float, step: float) -> float:
|
|
return min(max(target, current - step), current + step)
|
|
|
|
|
|
class ProfileSmoother:
|
|
"""Bound live setting/personality changes and fade invalid input to stock."""
|
|
def __init__(self):
|
|
self.applied: AppliedProfile | None = None
|
|
self.slc_braking_style = 'standard'
|
|
|
|
def sample(self, target: ProfileTuning | None, personality, dt: float, *, traffic_mode: bool | None = False) -> AppliedProfile | None:
|
|
self.slc_braking_style = 'standard'
|
|
if type(dt) not in (int, float) or not math.isfinite(dt) or not 0.0 < dt <= 0.25:
|
|
self.applied = None
|
|
return None
|
|
if type(traffic_mode) is not bool:
|
|
self.applied = None
|
|
return None
|
|
personality_id = active_personality_id(traffic_mode, personality)
|
|
if personality_id not in ('traffic', 'aggressive', 'standard', 'relaxed'):
|
|
self.applied = None
|
|
return None
|
|
if target is not None:
|
|
values = (target.follow_seconds, target.acceleration_jerk, target.deceleration_jerk,
|
|
target.speed_jerk, target.speed_decrease_jerk, target.danger_jerk)
|
|
valid = (target.personality_id == personality_id and
|
|
target.slc_braking_style in ('standard', 'eco', 'sport') and
|
|
type(target.traffic_braking_custom) is bool and
|
|
all(type(value) in (int, float) and math.isfinite(value) for value in values) and
|
|
FOLLOW_MIN_SECONDS <= target.follow_seconds <= FOLLOW_MAX_SECONDS and
|
|
all(JERK_MIN <= value <= JERK_MAX for value in values[1:]) and
|
|
(target.acceleration_max is None or
|
|
(type(target.acceleration_max) in (int, float) and math.isfinite(target.acceleration_max) and
|
|
0.0 <= target.acceleration_max <= ACCEL_MAX)) and
|
|
(target.cruise_brake_magnitude is None or
|
|
(type(target.cruise_brake_magnitude) in (int, float) and math.isfinite(target.cruise_brake_magnitude) and
|
|
(0.35 if traffic_mode else 0.5) <= target.cruise_brake_magnitude <= abs(ACCEL_MIN))))
|
|
if not valid:
|
|
target = None
|
|
if target is not None and not traffic_mode:
|
|
self.slc_braking_style = target.slc_braking_style
|
|
if traffic_mode and target is None:
|
|
self.applied = None
|
|
return None
|
|
default_follow = {'traffic': 0.75, 'aggressive': 1.25, 'standard': 1.45, 'relaxed': 1.75}[personality_id]
|
|
default_jerk = 0.5 if personality_id == 'aggressive' else 1.0
|
|
default = AppliedProfile(default_follow, ACCEL_MAX, 1.2, default_jerk, default_jerk,
|
|
default_jerk, default_jerk, 1.0)
|
|
if target is None and self.applied is None:
|
|
return None
|
|
if target is None:
|
|
desired = default
|
|
else:
|
|
desired = AppliedProfile(target.follow_seconds,
|
|
min(target.acceleration_max if target.acceleration_max is not None else ACCEL_MAX, ACCEL_MAX),
|
|
min(target.cruise_brake_magnitude if target.cruise_brake_magnitude is not None else 1.2,
|
|
abs(ACCEL_MIN)),
|
|
target.acceleration_jerk, target.deceleration_jerk,
|
|
target.speed_jerk, target.speed_decrease_jerk, target.danger_jerk)
|
|
if self.applied is None and traffic_mode:
|
|
# The first Traffic frame fades from the ordinary native personality,
|
|
# not from Traffic's low-speed endpoint (which may be below this target).
|
|
ordinary_id = active_personality_id(False, personality)
|
|
ordinary_follow = {'aggressive': 1.25, 'standard': 1.45, 'relaxed': 1.75}.get(ordinary_id)
|
|
if ordinary_follow is None:
|
|
return None
|
|
ordinary_jerk = 0.5 if ordinary_id == 'aggressive' else 1.0
|
|
previous = AppliedProfile(ordinary_follow, ACCEL_MAX, 1.2, ordinary_jerk, ordinary_jerk,
|
|
ordinary_jerk, ordinary_jerk, 1.0)
|
|
else:
|
|
previous = self.applied or default
|
|
follow_step = max(dt, 0.0) * 1.0
|
|
accel_step = max(dt, 0.0) * 2.0
|
|
jerk_step = max(dt, 0.0) * 2.0
|
|
self.applied = AppliedProfile(
|
|
_move(previous.follow_seconds, desired.follow_seconds, follow_step),
|
|
_move(previous.acceleration_max, desired.acceleration_max, accel_step),
|
|
_move(previous.cruise_brake_magnitude, desired.cruise_brake_magnitude, accel_step),
|
|
_move(previous.acceleration_jerk, desired.acceleration_jerk, jerk_step),
|
|
_move(previous.deceleration_jerk, desired.deceleration_jerk, jerk_step),
|
|
_move(previous.speed_jerk, desired.speed_jerk, jerk_step),
|
|
_move(previous.speed_decrease_jerk, desired.speed_decrease_jerk, jerk_step),
|
|
_move(previous.danger_jerk, desired.danger_jerk, jerk_step),
|
|
)
|
|
if target is None and all(abs(a - b) < 1e-8 for a, b in zip(astuple(self.applied), astuple(default), strict=True)):
|
|
self.applied = None
|
|
return self.applied
|
|
|
|
|
|
def read_settings(params) -> ProfileSettings | None:
|
|
"""A saved preference is inert unless the host explicitly opts into this runtime."""
|
|
master = read_master_value(params)
|
|
if not master.valid or master.value is not True:
|
|
return None
|
|
health = read_profile_health(params, master=master)
|
|
if not health.dependencies_valid:
|
|
return None
|
|
try:
|
|
profile_enabled = dict.fromkeys(NAMES, True)
|
|
follow = {name: (health.number(SCALARS[name][0]), health.number(SCALARS[name][1])) for name in NAMES}
|
|
jerk = {name: (health.number(SCALARS[name][2]) / 100.0, health.number(SCALARS[name][3]) / 100.0,
|
|
health.number(SCALARS[name][4]) / 100.0, health.number(SCALARS[name][5]) / 100.0,
|
|
health.number(SCALARS[name][6]) / 100.0) for name in NAMES}
|
|
return ProfileSettings(health.document, profile_enabled, follow, jerk)
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def read_traffic_settings(params) -> TrafficSettings:
|
|
health = read_traffic_health(params)
|
|
if not health.valid:
|
|
return TrafficSettings(False, health.reason, None, False, (0.75, 1.6), ())
|
|
follow = (health.number('TrafficFollow'), health.number('RelaxedFollow'))
|
|
jerk = tuple((health.number('Traffic' + suffix) / 100.0,
|
|
health.number('Relaxed' + suffix) / 100.0) for suffix in TRAFFIC_JERK_SUFFIXES)
|
|
return TrafficSettings(True, 'valid', health.document if health.master_on else None,
|
|
health.profile_enabled if health.master_on else False, follow, jerk)
|
|
|
|
|
|
def _traffic_value(points: tuple[float, float], speed: float) -> float:
|
|
ratio = min(max((speed - TRAFFIC_BP[0]) / (TRAFFIC_BP[1] - TRAFFIC_BP[0]), 0.0), 1.0)
|
|
return float(points[0] + (points[1] - points[0]) * ratio)
|
|
|
|
|
|
def resolve(settings: ProfileSettings | TrafficSettings | None, personality, v_ego: float, CP,
|
|
*, traffic_mode: bool | None = False) -> ProfileTuning | None:
|
|
if settings is None or type(traffic_mode) is not bool or not math.isfinite(v_ego) or v_ego < 0:
|
|
return None
|
|
personality_id = active_personality_id(traffic_mode, personality)
|
|
if traffic_mode:
|
|
if not isinstance(settings, TrafficSettings) or not settings.valid or len(settings.jerk) != 5:
|
|
return None
|
|
follow = _traffic_value(settings.follow, v_ego)
|
|
acceleration_max = interpolate_accel_profile(v_ego, A_CRUISE_MAX_VALS_TRAFFIC_ALL)
|
|
cruise_brake_magnitude = TRAFFIC_CRUISE_BRAKE_MAGNITUDE
|
|
traffic_braking_custom = False
|
|
document = settings.document
|
|
if document is not None and document['enabled'] and settings.profile_enabled:
|
|
categories = document['profiles']['traffic']
|
|
ev_tuning = CP.transmissionType == car.CarParams.TransmissionType.direct
|
|
truck_tuning = is_truck_fingerprint(str(CP.carFingerprint)) and not ev_tuning
|
|
for category in ('following', 'acceleration', 'braking'):
|
|
config = categories[category]
|
|
if config['preset'] in ('dom_default', 'selected_profile') or (category != 'following' and not config.get('legacyActivation', False)):
|
|
continue
|
|
value = interpolate_category_curve(category, v_ego, config, ev_tuning, truck_tuning)
|
|
if category == 'following':
|
|
follow = value
|
|
elif category == 'acceleration':
|
|
acceleration_max = value
|
|
else:
|
|
cruise_brake_magnitude = value
|
|
traffic_braking_custom = True
|
|
if (not all(math.isfinite(value) for value in (follow, acceleration_max, cruise_brake_magnitude)) or
|
|
not FOLLOW_MIN_SECONDS <= follow <= FOLLOW_MAX_SECONDS or
|
|
acceleration_max < 0.0 or not 0.35 <= cruise_brake_magnitude <= abs(ACCEL_MIN)):
|
|
return None
|
|
acceleration, deceleration, speed, speed_decrease, danger = (
|
|
_traffic_value(points, v_ego) for points in settings.jerk
|
|
)
|
|
return ProfileTuning('traffic', follow, acceleration, deceleration, speed, speed_decrease, danger,
|
|
min(acceleration_max, ACCEL_MAX), cruise_brake_magnitude,
|
|
traffic_braking_custom=traffic_braking_custom)
|
|
if not isinstance(settings, ProfileSettings):
|
|
return None
|
|
if personality_id not in settings.follow:
|
|
return None
|
|
low, high = settings.follow[personality_id]
|
|
follow = float(low + (high - low) * min(max((v_ego - LEGACY_FOLLOW_BP[0]) /
|
|
(LEGACY_FOLLOW_BP[1] - LEGACY_FOLLOW_BP[0]), 0.0), 1.0))
|
|
acceleration_max = None
|
|
cruise_brake_magnitude = None
|
|
slc_braking_style = 'standard'
|
|
custom_acceleration = False
|
|
document = settings.document
|
|
if document is not None and document['enabled'] and settings.profile_enabled[personality_id]:
|
|
categories = document['profiles'][personality_id]
|
|
if categories['braking'].get('legacyActivation', False) and categories['braking']['preset'] in ('eco', 'sport'):
|
|
slc_braking_style = categories['braking']['preset']
|
|
ev_tuning = CP.transmissionType == car.CarParams.TransmissionType.direct
|
|
truck_tuning = is_truck_fingerprint(str(CP.carFingerprint)) and not ev_tuning
|
|
for category in ('following', 'acceleration', 'braking'):
|
|
config = categories[category]
|
|
if config['preset'] in ('dom_default', 'selected_profile') or (category != 'following' and not config.get('legacyActivation', False)):
|
|
continue
|
|
value = interpolate_category_curve(category, v_ego, config, ev_tuning, truck_tuning)
|
|
if not math.isfinite(value):
|
|
return None
|
|
if category == 'following':
|
|
follow = value
|
|
elif category == 'acceleration':
|
|
custom_acceleration = config['preset'] == 'custom'
|
|
acceleration_max = min(max(value, 0.0), ACCEL_MAX)
|
|
else:
|
|
cruise_brake_magnitude = min(max(value, 0.5), abs(ACCEL_MIN))
|
|
acceleration_jerk, deceleration_jerk, speed_jerk, speed_decrease_jerk, danger_jerk = settings.jerk[personality_id]
|
|
return ProfileTuning(personality_id, min(max(follow, FOLLOW_MIN_SECONDS), FOLLOW_MAX_SECONDS),
|
|
acceleration_jerk, deceleration_jerk, speed_jerk, speed_decrease_jerk, danger_jerk,
|
|
acceleration_max, cruise_brake_magnitude, slc_braking_style, custom_acceleration=custom_acceleration)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SelectedProfileTuning:
|
|
acceleration_max: float | None = None
|
|
cruise_brake_magnitude: float | None = None
|
|
braking_style: str = 'standard'
|
|
|
|
|
|
def selected_profiles_requested(document: dict | None) -> bool:
|
|
if document is None:
|
|
return False
|
|
if document['selectedAccelerationProfile'] != 'dom_default' or document['selectedDecelerationProfile'] != 'dom_default':
|
|
return True
|
|
return any(config['preset'] not in ('dom_default', 'selected_profile') and not config.get('legacyActivation', False)
|
|
for profile in document['profiles'].values() for category, config in profile.items() if category != 'following')
|
|
|
|
|
|
def resolve_selected_profiles(document: dict | None, personality, v_ego: float, CP, *, traffic_mode: bool | None = False,
|
|
legacy: ProfileTuning | None = None) -> SelectedProfileTuning | None:
|
|
"""Resolve only acceleration/braking; never enable following or jerk tuning.
|
|
|
|
Migration provenance preserves the former master/curve gates until the user
|
|
explicitly selects inheritance or an override. Global deceleration retains
|
|
the historical response scale; explicit personality curves retain theirs.
|
|
"""
|
|
identity = active_personality_id(traffic_mode, personality)
|
|
if document is None or identity is None or not math.isfinite(v_ego) or v_ego < 0:
|
|
return None
|
|
ev = CP.transmissionType == car.CarParams.TransmissionType.direct
|
|
truck = is_truck_fingerprint(str(CP.carFingerprint)) and not ev
|
|
acceleration = braking = None
|
|
style = 'standard'
|
|
for category in ('acceleration', 'braking'):
|
|
config = document['profiles'][identity][category]
|
|
preset = config['preset']
|
|
inherited = preset == 'selected_profile'
|
|
if config.get('legacyActivation', False):
|
|
# Old active overrides are already applied by the original smoother.
|
|
active_value = getattr(legacy, 'acceleration_max' if category == 'acceleration' else 'cruise_brake_magnitude', None)
|
|
if active_value is not None or category == 'acceleration' or traffic_mode is True:
|
|
continue
|
|
inherited = True # Old global response was ordinary-cruise-only.
|
|
if inherited:
|
|
preset = document['selectedAccelerationProfile' if category == 'acceleration' else 'selectedDecelerationProfile']
|
|
if preset == 'dom_default':
|
|
continue
|
|
if category == 'braking' and inherited:
|
|
value = {'standard': 1.2, 'eco': 0.6, 'sport': 2.4}[preset]
|
|
else:
|
|
value = interpolate_category_curve(category, v_ego,
|
|
{'preset': preset, 'curve': []} if inherited else config, ev, truck)
|
|
if not math.isfinite(value):
|
|
return None
|
|
if category == 'acceleration':
|
|
acceleration = min(max(value, 0.0), ACCEL_MAX)
|
|
else:
|
|
braking = min(max(value, 0.35 if traffic_mode else 0.5), abs(ACCEL_MIN))
|
|
style = preset if preset in ('eco', 'sport') else 'standard'
|
|
return SelectedProfileTuning(acceleration, braking, style)
|
|
|
|
|
|
class ProfileHost:
|
|
"""Bounded local Params refresh; a failed read expires the previous tuning."""
|
|
REFRESH_NS = 1_000_000_000
|
|
MAX_AGE_NS = 2_000_000_000
|
|
|
|
def __init__(self, params):
|
|
self.params = params
|
|
self.disabled = False
|
|
self.selected_document = None
|
|
self.selected_attempt_ns = -self.REFRESH_NS
|
|
self.selected_success_ns = -1
|
|
self.settings: ProfileSettings | TrafficSettings | None = None
|
|
self.last_attempt_ns = -self.REFRESH_NS
|
|
self.last_success_ns = -1
|
|
|
|
self.last_mode: bool | None = None
|
|
self.global_braking_response: str | None = None
|
|
self.global_last_attempt_ns = -self.REFRESH_NS
|
|
self.global_last_success_ns = -1
|
|
|
|
def sample_selected(self, now_ns: int, personality, v_ego: float, CP, *, traffic_mode: bool | None = False,
|
|
legacy: ProfileTuning | None = None) -> SelectedProfileTuning | None:
|
|
if now_ns < 0 or now_ns < self.selected_attempt_ns:
|
|
self.selected_document = None
|
|
self.selected_attempt_ns = now_ns - self.REFRESH_NS
|
|
self.selected_success_ns = -1
|
|
if now_ns - self.selected_attempt_ns >= self.REFRESH_NS:
|
|
self.selected_attempt_ns = now_ns
|
|
try:
|
|
saved = read_document_value(self.params)
|
|
self.selected_document = saved.value if saved.valid else None
|
|
self.selected_success_ns = now_ns if saved.valid else -1
|
|
except (OSError, RuntimeError, ValueError, TypeError, KeyError):
|
|
self.selected_document = None
|
|
self.selected_success_ns = -1
|
|
if self.selected_success_ns < 0 or now_ns - self.selected_success_ns > self.MAX_AGE_NS:
|
|
return None
|
|
return resolve_selected_profiles(self.selected_document, personality, v_ego, CP,
|
|
traffic_mode=traffic_mode, legacy=legacy)
|
|
|
|
def sample_global_braking(self, now_ns: int) -> str | None:
|
|
if now_ns < 0 or now_ns < self.global_last_attempt_ns:
|
|
self.global_braking_response = None
|
|
self.global_last_attempt_ns = now_ns - self.REFRESH_NS
|
|
self.global_last_success_ns = -1
|
|
if now_ns - self.global_last_attempt_ns >= self.REFRESH_NS:
|
|
self.global_last_attempt_ns = now_ns
|
|
try:
|
|
saved = read_document_value(self.params)
|
|
if saved.valid and saved.value is None:
|
|
self.global_braking_response = "standard"
|
|
elif saved.valid and isinstance(saved.value, dict):
|
|
response = saved.value.get("selectedDecelerationProfile")
|
|
self.global_braking_response = response if isinstance(response, str) else None
|
|
else:
|
|
self.global_braking_response = None
|
|
self.global_last_success_ns = now_ns if self.global_braking_response is not None else -1
|
|
except (OSError, RuntimeError, ValueError, TypeError, KeyError):
|
|
self.global_braking_response = None
|
|
self.global_last_success_ns = -1
|
|
if self.global_last_success_ns < 0 or now_ns - self.global_last_success_ns > self.MAX_AGE_NS:
|
|
return None
|
|
return self.global_braking_response
|
|
|
|
def sample(self, now_ns: int, personality, v_ego: float, CP, *, traffic_mode: bool | None = False) -> ProfileTuning | None:
|
|
if type(traffic_mode) is not bool:
|
|
self.disabled = False
|
|
self.settings = None
|
|
self.last_attempt_ns = now_ns - self.REFRESH_NS
|
|
self.last_success_ns = -1
|
|
self.last_mode = None
|
|
return None
|
|
if self.last_mode is not traffic_mode:
|
|
self.settings = None
|
|
self.last_attempt_ns = now_ns - self.REFRESH_NS
|
|
self.last_success_ns = -1
|
|
self.last_mode = traffic_mode
|
|
if now_ns < 0 or now_ns < self.last_attempt_ns:
|
|
self.disabled = False
|
|
self.settings = None
|
|
self.last_attempt_ns = now_ns - self.REFRESH_NS
|
|
self.last_success_ns = -1
|
|
if now_ns - self.last_attempt_ns >= self.REFRESH_NS:
|
|
self.last_attempt_ns = now_ns
|
|
try:
|
|
master = read_master_value(self.params)
|
|
self.disabled = (not traffic_mode and master.valid and master.value is False and
|
|
read_document_value(self.params).valid)
|
|
self.settings = read_traffic_settings(self.params) if traffic_mode else read_settings(self.params)
|
|
self.last_success_ns = now_ns
|
|
except (OSError, RuntimeError, ValueError, TypeError):
|
|
self.disabled = False
|
|
self.settings = None
|
|
if self.last_success_ns < 0 or now_ns - self.last_success_ns > self.MAX_AGE_NS:
|
|
self.disabled = False
|
|
return None
|
|
return resolve(self.settings, personality, v_ego, CP, traffic_mode=traffic_mode)
|