mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 05:23:41 +08:00
e4f3d6661c
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>
187 lines
6.7 KiB
Python
187 lines
6.7 KiB
Python
"""Strict saved Long Planner health shared by the runtime and native editor."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
import math
|
|
|
|
from openpilot.starpilot.longitudinal.profile_document import (
|
|
PERSONALITY_PROFILES_PARAM, default_personality_profiles, profile_document,
|
|
is_unconfigured_profile_document, migrate_profile_document,
|
|
)
|
|
from openpilot.starpilot.saved_source import read_saved
|
|
|
|
NAMES = ("aggressive", "standard", "relaxed")
|
|
TRAFFIC_JERK_SUFFIXES = ("JerkAcceleration", "JerkDeceleration", "JerkSpeed",
|
|
"JerkSpeedDecrease", "JerkDanger")
|
|
TRAFFIC_NUMBER_KEYS = ("TrafficFollow", "RelaxedFollow",
|
|
*("Traffic" + suffix for suffix in TRAFFIC_JERK_SUFFIXES),
|
|
*("Relaxed" + suffix for suffix in TRAFFIC_JERK_SUFFIXES))
|
|
SUFFIXES = ("Follow", "FollowHigh", "JerkAcceleration", "JerkDeceleration",
|
|
"JerkSpeed", "JerkSpeedDecrease", "JerkDanger")
|
|
SCALARS = {name: tuple(name.title() + suffix for suffix in SUFFIXES) for name in NAMES}
|
|
FLAGS = ()
|
|
KEYS = ("CustomPersonalities", PERSONALITY_PROFILES_PARAM, *FLAGS,
|
|
*(key for name in NAMES for key in SCALARS[name]))
|
|
FOLLOW_MIN_SECONDS = 0.5
|
|
TRAFFIC_SAVED_MIN_SECONDS = 0.5
|
|
FOLLOW_MAX_SECONDS = 3.0
|
|
JERK_MIN_PERCENT = 25.0
|
|
JERK_MAX_PERCENT = 200.0
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SavedValue:
|
|
raw: bytes | None
|
|
value: bool | float | dict | None
|
|
valid: bool
|
|
readable: bool
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ProfileHealth:
|
|
values: dict[str, SavedValue]
|
|
|
|
def number(self, key: str) -> float:
|
|
saved = self.values[key]
|
|
if saved.valid and isinstance(saved.value, float):
|
|
return saved.value
|
|
raise ValueError(f"Invalid saved longitudinal setting: {key}")
|
|
|
|
@property
|
|
def document(self) -> dict | None:
|
|
saved = self.values[PERSONALITY_PROFILES_PARAM]
|
|
return saved.value if saved.valid and isinstance(saved.value, dict) else None
|
|
|
|
@property
|
|
def dependencies_valid(self) -> bool:
|
|
return all(value.valid for key, value in self.values.items() if key != "CustomPersonalities")
|
|
|
|
@property
|
|
def master_on(self) -> bool:
|
|
return self.values["CustomPersonalities"].value is True and self.values["CustomPersonalities"].valid
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class TrafficHealth:
|
|
values: dict[str, SavedValue]
|
|
reason: str
|
|
|
|
@property
|
|
def valid(self) -> bool:
|
|
return self.reason == "valid"
|
|
|
|
@property
|
|
def master_on(self) -> bool:
|
|
return self.values["CustomPersonalities"].value is True
|
|
|
|
def number(self, key: str) -> float:
|
|
saved = self.values[key]
|
|
if saved.valid and isinstance(saved.value, float):
|
|
return saved.value
|
|
raise ValueError(f"Invalid saved Traffic setting: {key}")
|
|
|
|
@property
|
|
def document(self) -> dict | None:
|
|
saved = self.values.get(PERSONALITY_PROFILES_PARAM)
|
|
return saved.value if saved is not None and saved.valid and isinstance(saved.value, dict) else None
|
|
|
|
@property
|
|
def profile_enabled(self) -> bool:
|
|
return True
|
|
|
|
|
|
def _read(params, key: str) -> tuple[bytes | None, bool]:
|
|
limit = 65536 if key == PERSONALITY_PROFILES_PARAM else 128
|
|
return read_saved(params, key, limit)
|
|
|
|
|
|
def _bool_value(raw: bytes | None, default: object) -> SavedValue:
|
|
if raw is None:
|
|
return SavedValue(None, default if isinstance(default, bool) else None, isinstance(default, bool), True)
|
|
valid = raw in (b"0", b"1")
|
|
return SavedValue(raw, raw == b"1" if valid else None, valid, True)
|
|
|
|
|
|
def read_master_value(params) -> SavedValue:
|
|
raw, readable = _read(params, "CustomPersonalities")
|
|
return (_bool_value(raw, params.get_default_value("CustomPersonalities")) if readable else
|
|
SavedValue(raw, None, False, False))
|
|
|
|
|
|
def read_document_value(params) -> SavedValue:
|
|
raw, readable = _read(params, PERSONALITY_PROFILES_PARAM)
|
|
if not readable:
|
|
return SavedValue(raw, None, False, False)
|
|
if raw is None:
|
|
default = params.get_default_value(PERSONALITY_PROFILES_PARAM)
|
|
valid = default in (None, {})
|
|
document = profile_document(default_personality_profiles(False), enabled=False) if valid else None
|
|
return SavedValue(None, document, valid, True)
|
|
try:
|
|
document = migrate_profile_document(raw)
|
|
valid = document is not None or is_unconfigured_profile_document(raw)
|
|
except (RecursionError, ValueError, OverflowError):
|
|
document, valid = None, False
|
|
return SavedValue(raw, document, valid, True)
|
|
|
|
|
|
def _scalar_value(key: str, raw: bytes | None, default: object) -> SavedValue:
|
|
try:
|
|
number = float(raw) if raw is not None else default if isinstance(default, (int, float)) and not isinstance(default, bool) else None
|
|
low, high = (((TRAFFIC_SAVED_MIN_SECONDS if key == "TrafficFollow" else FOLLOW_MIN_SECONDS), FOLLOW_MAX_SECONDS)
|
|
if "Follow" in key else
|
|
(JERK_MIN_PERCENT, JERK_MAX_PERCENT))
|
|
valid = number is not None and math.isfinite(number) and low <= number <= high
|
|
except (ValueError, OverflowError):
|
|
number, valid = None, False
|
|
return SavedValue(raw, float(number) if valid and number is not None else None, valid, True)
|
|
|
|
|
|
def read_profile_health(params, *, master: SavedValue | None = None) -> ProfileHealth:
|
|
values: dict[str, SavedValue] = {}
|
|
values["CustomPersonalities"] = master if master is not None else read_master_value(params)
|
|
for key in KEYS:
|
|
if key == "CustomPersonalities":
|
|
continue
|
|
if key == PERSONALITY_PROFILES_PARAM:
|
|
values[key] = read_document_value(params)
|
|
continue
|
|
raw, readable = _read(params, key)
|
|
if not readable:
|
|
values[key] = SavedValue(raw, None, False, False)
|
|
continue
|
|
if raw is None:
|
|
default = params.get_default_value(key)
|
|
if key in FLAGS:
|
|
values[key] = _bool_value(None, default)
|
|
else:
|
|
values[key] = _scalar_value(key, None, default)
|
|
continue
|
|
if key in FLAGS:
|
|
values[key] = _bool_value(raw, None)
|
|
else:
|
|
values[key] = _scalar_value(key, raw, None)
|
|
return ProfileHealth(values)
|
|
|
|
|
|
def read_traffic_health(params) -> TrafficHealth:
|
|
"""Read only Traffic dependencies; unrelated profile corruption stays local."""
|
|
master = read_master_value(params)
|
|
values = {"CustomPersonalities": master}
|
|
if not master.valid:
|
|
return TrafficHealth(values, "invalid_master")
|
|
custom = master.value is True
|
|
if custom:
|
|
document = read_document_value(params)
|
|
values[PERSONALITY_PROFILES_PARAM] = document
|
|
for key in TRAFFIC_NUMBER_KEYS:
|
|
raw, readable = _read(params, key) if custom else (None, True)
|
|
values[key] = (_scalar_value(key, raw, params.get_default_value(key))
|
|
if readable else SavedValue(raw, None, False, False))
|
|
bad = next((key for key, value in values.items() if not value.valid), None)
|
|
if bad is not None:
|
|
return TrafficHealth(values, f"invalid_{bad}")
|
|
health = TrafficHealth(values, "valid")
|
|
return health
|