mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-27 09:53:45 +08:00
624 lines
24 KiB
Python
624 lines
24 KiB
Python
#!/usr/bin/env python3
|
|
"""Versioned, fail-closed longitudinal acceleration/braking profiles."""
|
|
from __future__ import annotations
|
|
|
|
from copy import deepcopy
|
|
import json
|
|
import math
|
|
import numbers
|
|
|
|
PERSONALITY_PROFILES_PARAM = "LongitudinalPersonalityProfiles"
|
|
PROFILE_SCHEMA_VERSION = 3
|
|
PERSONALITY_IDS = ("traffic", "aggressive", "standard", "relaxed")
|
|
TRUCK_FINGERPRINT_TOKENS = (
|
|
" RAM 1500 ",
|
|
" RAM HD ",
|
|
" F 150 ",
|
|
" MAVERICK ",
|
|
" RANGER ",
|
|
" SILVERADO ",
|
|
" RIDGELINE ",
|
|
" SANTA CRUZ ",
|
|
)
|
|
|
|
ACCELERATION_SPEEDS_MPH = tuple(range(0, 91, 10))
|
|
BRAKING_SPEEDS_MPH = ACCELERATION_SPEEDS_MPH
|
|
FOLLOWING_SPEEDS_MPH = ACCELERATION_SPEEDS_MPH
|
|
_NATIVE_ACCELERATION_SPEEDS_MS = (0.0, 5.0, 10.0, 15.0, 20.0, 25.0, 40.0)
|
|
_V1_ACCELERATION_SPEEDS_MPH = (0.0, 11.184681, 22.369363, 33.554044, 44.738726, 55.923407, 89.477452)
|
|
|
|
|
|
def is_truck_fingerprint(fingerprint: object) -> bool:
|
|
if not isinstance(fingerprint, str) or not fingerprint.strip():
|
|
return False
|
|
normalized = f" {fingerprint.strip().upper().replace('_', ' ').replace('-', ' ')} "
|
|
return any(token in normalized for token in TRUCK_FINGERPRINT_TOKENS)
|
|
|
|
ACCELERATION_PRESETS = ("dom_default", "standard", "eco", "sport", "sport_plus", "custom")
|
|
BRAKING_PRESETS = ("dom_default", "standard", "eco", "sport", "custom")
|
|
_LEGACY_FOLLOWING_PRESETS = ("close", "medium", "far")
|
|
FOLLOWING_PRESETS = ("dom_default", "close", "medium", "far", "traffic", "custom", "legacy_close", "legacy_medium", "legacy_far")
|
|
CURVE_BOUNDS = {
|
|
"acceleration": (0.0, 3.5),
|
|
"braking": (0.5, 2.0),
|
|
"following": (0.75, 3.0),
|
|
}
|
|
_V2_CURVE_BOUNDS = {
|
|
"acceleration": (0.0, 6.0),
|
|
"braking": (0.5, 2.0),
|
|
"following": (0.75, 3.0),
|
|
}
|
|
_V1_CURVE_BOUNDS = dict(_V2_CURVE_BOUNDS)
|
|
# Dom's Traffic default is below the point editor's minimum. Keep it losslessly
|
|
# after initialization/reset, while applying CURVE_BOUNDS to newly edited points.
|
|
_V3_CURVE_BOUNDS = {**_V2_CURVE_BOUNDS, "braking": (0.35, 2.0), "following": (0.5, 3.0)}
|
|
PERSONALITY_ADVANCED_PARAM_KEYS = frozenset(
|
|
f"{profile}{suffix}"
|
|
for profile in ("Traffic", "Aggressive", "Standard", "Relaxed")
|
|
for suffix in ("JerkAcceleration", "JerkDeceleration", "JerkDanger", "JerkSpeedDecrease", "JerkSpeed")
|
|
)
|
|
PERSONALITY_FOLLOW_PARAM_KEYS = frozenset({
|
|
"TrafficFollow",
|
|
"AggressiveFollow", "AggressiveFollowHigh",
|
|
"StandardFollow", "StandardFollowHigh",
|
|
"RelaxedFollow", "RelaxedFollowHigh",
|
|
})
|
|
PERSONALITY_PROFILE_ENABLE_RUNTIME_KEYS = (
|
|
("traffic_personality_profile", "TrafficPersonalityProfile"),
|
|
("aggressive_personality_profile", "AggressivePersonalityProfile"),
|
|
("standard_personality_profile", "StandardPersonalityProfile"),
|
|
("relaxed_personality_profile", "RelaxedPersonalityProfile"),
|
|
)
|
|
PERSONALITY_PROFILE_ENABLE_PARAM_KEYS = frozenset(
|
|
param_key for _runtime_key, param_key in PERSONALITY_PROFILE_ENABLE_RUNTIME_KEYS
|
|
)
|
|
PERSONALITY_PARKED_PARAM_KEYS = (
|
|
PERSONALITY_ADVANCED_PARAM_KEYS
|
|
| PERSONALITY_FOLLOW_PARAM_KEYS
|
|
| PERSONALITY_PROFILE_ENABLE_PARAM_KEYS
|
|
| {"CustomPersonalities"}
|
|
)
|
|
|
|
|
|
def load_personality_profile_enable_values(get_value) -> dict[str, bool]:
|
|
return {
|
|
runtime_key: get_value(param_key)
|
|
for runtime_key, param_key in PERSONALITY_PROFILE_ENABLE_RUNTIME_KEYS
|
|
}
|
|
|
|
|
|
def validate_personality_follow_value(raw_value) -> float:
|
|
if not isinstance(raw_value, numbers.Real) or isinstance(raw_value, bool):
|
|
raise ValueError("Following values must be JSON numbers.")
|
|
value = float(raw_value)
|
|
if not math.isfinite(value) or value < 0.5 or value > 3.0:
|
|
raise ValueError("Following values must be between 0.5 and 3.0.")
|
|
return round(value, 4)
|
|
|
|
|
|
def validate_personality_advanced_value(raw_value) -> float:
|
|
if not isinstance(raw_value, numbers.Real) or isinstance(raw_value, bool):
|
|
raise ValueError("Advanced personality values must be JSON numbers.")
|
|
value = float(raw_value)
|
|
if not math.isfinite(value) or value < 25.0 or value > 200.0:
|
|
raise ValueError("Advanced personality values must be between 25 and 200.")
|
|
return round(value, 4)
|
|
|
|
|
|
_CATEGORY_SPECS = {
|
|
"acceleration": (ACCELERATION_PRESETS, len(ACCELERATION_SPEEDS_MPH)),
|
|
"braking": (BRAKING_PRESETS, len(BRAKING_SPEEDS_MPH)),
|
|
"following": (FOLLOWING_PRESETS, len(FOLLOWING_SPEEDS_MPH)),
|
|
}
|
|
|
|
_BRAKING_PRESET_CURVES = {
|
|
"eco": (0.5,) * len(BRAKING_SPEEDS_MPH),
|
|
"standard": (1.0,) * len(BRAKING_SPEEDS_MPH),
|
|
"sport": (2.0,) * len(BRAKING_SPEEDS_MPH),
|
|
}
|
|
# Named following presets use Dom's native speed breakpoints. Custom retains
|
|
# its editable 10 mph grid; sampling named presets onto that grid is only for
|
|
# first-use Custom conversion and graph previews, not runtime interpolation.
|
|
FOLLOWING_PRESET_CURVES = {
|
|
"close": (1.25, 1.0),
|
|
"medium": (1.45, 1.2),
|
|
"far": (1.6, 1.4),
|
|
"traffic": (0.75, 1.6),
|
|
"legacy_close": (1.25, 1.25),
|
|
"legacy_medium": (1.45, 1.45),
|
|
"legacy_far": (1.75, 1.75),
|
|
}
|
|
_FOLLOWING_PRESET_SPEEDS_MPH = {
|
|
preset: (0.0, 25.0 / 0.44704) if preset == "traffic" else (45.0, 70.0)
|
|
for preset in FOLLOWING_PRESET_CURVES
|
|
}
|
|
PROFILE_AXES = {
|
|
"acceleration": {
|
|
"speed": {"unit": "mph", "values": list(ACCELERATION_SPEEDS_MPH)},
|
|
"value": {"unit": "m/s^2", "meaning": "maximum_requested_acceleration"},
|
|
},
|
|
"braking": {
|
|
"speed": {"unit": "mph", "values": list(BRAKING_SPEEDS_MPH)},
|
|
"value": {"unit": "m/s^2", "meaning": "cruise_slc_deceleration_magnitude"},
|
|
},
|
|
"following": {
|
|
"speed": {"unit": "mph", "values": list(FOLLOWING_SPEEDS_MPH)},
|
|
"value": {"unit": "s", "meaning": "base_time_headway"},
|
|
},
|
|
}
|
|
_CATEGORY_SPEEDS_MPH = {
|
|
"acceleration": ACCELERATION_SPEEDS_MPH,
|
|
"braking": BRAKING_SPEEDS_MPH,
|
|
"following": FOLLOWING_SPEEDS_MPH,
|
|
}
|
|
|
|
_ACCELERATION_PROFILE_IDS = {
|
|
"standard": 0,
|
|
"eco": 1,
|
|
"sport": 2,
|
|
"sport_plus": 3,
|
|
}
|
|
|
|
_PERSONALITY_REFERENCE_PRESETS = {
|
|
"traffic": {"acceleration": "eco", "braking": "standard", "following": "traffic"},
|
|
"aggressive": {"acceleration": "sport_plus", "braking": "sport", "following": "close"},
|
|
"standard": {"acceleration": "standard", "braking": "standard", "following": "medium"},
|
|
"relaxed": {"acceleration": "eco", "braking": "eco", "following": "far"},
|
|
}
|
|
|
|
_V1_PROFILE_AXES = {
|
|
"acceleration": {
|
|
"speed": {"unit": "mph", "values": list(_V1_ACCELERATION_SPEEDS_MPH)},
|
|
"value": {"unit": "m/s^2", "meaning": "maximum_requested_acceleration"},
|
|
},
|
|
"braking": {
|
|
"speed": {"unit": "mph", "values": list(_V1_ACCELERATION_SPEEDS_MPH)},
|
|
"value": {"unit": "m/s^2", "meaning": "cruise_slc_deceleration_magnitude"},
|
|
},
|
|
"following": {
|
|
"speed": {"unit": "mph", "values": list(FOLLOWING_SPEEDS_MPH)},
|
|
"value": {"unit": "s", "meaning": "base_time_headway"},
|
|
},
|
|
}
|
|
|
|
|
|
def _acceleration_preset_curve(preset: str, ev_tuning: bool, truck_tuning: bool) -> list[float]:
|
|
from openpilot.starpilot.common.accel_profile import get_accel_profile_curve_values
|
|
|
|
return get_accel_profile_curve_values(
|
|
_ACCELERATION_PROFILE_IDS[preset], bool(ev_tuning), bool(truck_tuning) and not bool(ev_tuning)
|
|
)
|
|
|
|
|
|
def default_personality_profiles(ev_tuning: bool, truck_tuning: bool = False) -> dict[str, dict]:
|
|
del ev_tuning, truck_tuning
|
|
return {
|
|
personality: {
|
|
"acceleration": {"preset": "dom_default", "curve": []},
|
|
"braking": {"preset": "dom_default", "curve": []},
|
|
"following": {"preset": "dom_default", "curve": []},
|
|
}
|
|
for personality in PERSONALITY_IDS
|
|
}
|
|
|
|
|
|
def profile_document(profiles: dict[str, dict], *, enabled: bool) -> dict:
|
|
if type(enabled) is not bool:
|
|
raise ValueError("enabled must be a JSON boolean")
|
|
return {
|
|
"schemaVersion": PROFILE_SCHEMA_VERSION,
|
|
"enabled": enabled,
|
|
"axes": deepcopy(PROFILE_AXES),
|
|
"profiles": deepcopy(profiles),
|
|
}
|
|
|
|
|
|
def _decode_json(raw):
|
|
if isinstance(raw, bytes):
|
|
try:
|
|
raw = raw.decode("utf-8", errors="strict")
|
|
except UnicodeDecodeError:
|
|
return None
|
|
if isinstance(raw, str):
|
|
try:
|
|
raw = json.loads(raw)
|
|
except (TypeError, ValueError, json.JSONDecodeError):
|
|
return None
|
|
return raw
|
|
|
|
|
|
def _validated_category_with_length(
|
|
category: str,
|
|
raw_category,
|
|
expected_length: int,
|
|
curve_bounds: dict[str, tuple[float, float]],
|
|
legacy_curve_bounds: dict[str, tuple[float, float]] | None = None,
|
|
*, retain_custom: bool = False,
|
|
) -> dict | None:
|
|
if category not in _CATEGORY_SPECS or not isinstance(raw_category, dict):
|
|
return None
|
|
keys = set(raw_category)
|
|
has_legacy_curve = "legacyCurve" in keys
|
|
if keys != ({"preset", "curve", "legacyCurve"} if has_legacy_curve else {"preset", "curve"}):
|
|
return None
|
|
presets, _ = _CATEGORY_SPECS[category]
|
|
preset = raw_category.get("preset")
|
|
curve = raw_category.get("curve")
|
|
if not isinstance(preset, str) or preset not in presets or not isinstance(curve, list):
|
|
return None
|
|
if preset != "custom" and (not retain_custom or not curve):
|
|
return {"preset": preset, "curve": []} if not curve and not has_legacy_curve else None
|
|
if len(curve) != expected_length:
|
|
return None
|
|
|
|
minimum, maximum = curve_bounds[category]
|
|
values = []
|
|
for raw_value in curve:
|
|
if isinstance(raw_value, bool) or not isinstance(raw_value, numbers.Real):
|
|
return None
|
|
value = float(raw_value)
|
|
if not math.isfinite(value) or not minimum <= value <= maximum:
|
|
return None
|
|
values.append(round(value, 4))
|
|
validated = {"preset": preset, "curve": values}
|
|
if has_legacy_curve:
|
|
legacy_curve = raw_category.get("legacyCurve")
|
|
if category not in ("acceleration", "braking") or expected_length != len(ACCELERATION_SPEEDS_MPH) or not isinstance(legacy_curve, list):
|
|
return None
|
|
if len(legacy_curve) != len(_V1_ACCELERATION_SPEEDS_MPH):
|
|
return None
|
|
legacy_minimum, legacy_maximum = (legacy_curve_bounds or curve_bounds)[category]
|
|
legacy_values = []
|
|
for raw_value in legacy_curve:
|
|
if isinstance(raw_value, bool) or not isinstance(raw_value, numbers.Real):
|
|
return None
|
|
value = float(raw_value)
|
|
if not math.isfinite(value) or not legacy_minimum <= value <= legacy_maximum:
|
|
return None
|
|
legacy_values.append(round(value, 4))
|
|
validated["legacyCurve"] = legacy_values
|
|
return validated
|
|
|
|
|
|
def _validated_category(category: str, raw_category) -> dict | None:
|
|
expected_length = _CATEGORY_SPECS.get(category, ((), 0))[1]
|
|
return _validated_category_with_length(category, raw_category, expected_length, _V3_CURVE_BOUNDS, _V1_CURVE_BOUNDS, retain_custom=True)
|
|
|
|
|
|
def _schema_values_equal(actual, expected) -> bool:
|
|
if type(actual) is not type(expected):
|
|
return False
|
|
if isinstance(expected, dict):
|
|
return set(actual) == set(expected) and all(_schema_values_equal(actual[key], expected[key]) for key in expected)
|
|
if isinstance(expected, list):
|
|
return len(actual) == len(expected) and all(_schema_values_equal(value, reference) for value, reference in zip(actual, expected, strict=True))
|
|
return actual == expected
|
|
|
|
|
|
def _strict_document(
|
|
raw_document,
|
|
schema_version: int,
|
|
axes: dict,
|
|
category_lengths: dict[str, int],
|
|
curve_bounds: dict[str, tuple[float, float]],
|
|
legacy_curve_bounds: dict[str, tuple[float, float]] | None = None,
|
|
) -> dict | None:
|
|
decoded = _decode_json(raw_document)
|
|
if not isinstance(decoded, dict) or set(decoded) != {"schemaVersion", "enabled", "axes", "profiles"}:
|
|
return None
|
|
if type(decoded["schemaVersion"]) is not int or decoded["schemaVersion"] != schema_version:
|
|
return None
|
|
if type(decoded["enabled"]) is not bool or not _schema_values_equal(decoded["axes"], axes):
|
|
return None
|
|
|
|
raw_profiles = decoded["profiles"]
|
|
if not isinstance(raw_profiles, dict) or set(raw_profiles) != set(PERSONALITY_IDS):
|
|
return None
|
|
profiles = {}
|
|
for personality in PERSONALITY_IDS:
|
|
raw_profile = raw_profiles.get(personality)
|
|
if not isinstance(raw_profile, dict) or set(raw_profile) != set(_CATEGORY_SPECS):
|
|
return None
|
|
profile = {}
|
|
for category in _CATEGORY_SPECS:
|
|
raw_category = raw_profile.get(category)
|
|
if (schema_version < 3 and category == "following" and isinstance(raw_category, dict)
|
|
and raw_category.get("preset") not in ("dom_default", "custom", *_LEGACY_FOLLOWING_PRESETS)):
|
|
return None
|
|
validated = _validated_category_with_length(
|
|
category, raw_profile.get(category), category_lengths[category], curve_bounds, legacy_curve_bounds,
|
|
retain_custom=schema_version >= 3,
|
|
)
|
|
if validated is None:
|
|
return None
|
|
profile[category] = validated
|
|
profiles[personality] = profile
|
|
return {
|
|
"schemaVersion": schema_version,
|
|
"enabled": decoded["enabled"],
|
|
"axes": deepcopy(axes),
|
|
"profiles": profiles,
|
|
}
|
|
|
|
|
|
def _preserve_legacy_following_presets(profiles: dict[str, dict]) -> None:
|
|
for profile in profiles.values():
|
|
config = profile["following"]
|
|
if config["preset"] in _LEGACY_FOLLOWING_PRESETS:
|
|
config["preset"] = "legacy_" + config["preset"]
|
|
|
|
|
|
def strict_profile_document(raw_document) -> dict | None:
|
|
# V2 uses fixed-distance named following presets. Preserve their original
|
|
# meaning; adopting a speed-dependent preset requires an explicit selection.
|
|
decoded = _decode_json(raw_document)
|
|
version = decoded.get("schemaVersion") if isinstance(decoded, dict) else None
|
|
if type(version) is not int or version not in (2, PROFILE_SCHEMA_VERSION):
|
|
return None
|
|
document = _strict_document(
|
|
decoded,
|
|
version,
|
|
PROFILE_AXES,
|
|
{category: expected_length for category, (_, expected_length) in _CATEGORY_SPECS.items()},
|
|
_V2_CURVE_BOUNDS if version == 2 else _V3_CURVE_BOUNDS,
|
|
_V1_CURVE_BOUNDS,
|
|
)
|
|
if document is not None:
|
|
if version == 2:
|
|
_preserve_legacy_following_presets(document["profiles"])
|
|
document["schemaVersion"] = PROFILE_SCHEMA_VERSION
|
|
return document
|
|
|
|
|
|
def migrate_profile_document(raw_document) -> dict | None:
|
|
current = strict_profile_document(raw_document)
|
|
if current is not None:
|
|
return current
|
|
|
|
legacy = _strict_document(
|
|
raw_document,
|
|
1,
|
|
_V1_PROFILE_AXES,
|
|
{"acceleration": len(_V1_ACCELERATION_SPEEDS_MPH), "braking": len(_V1_ACCELERATION_SPEEDS_MPH), "following": len(FOLLOWING_SPEEDS_MPH)},
|
|
_V1_CURVE_BOUNDS,
|
|
)
|
|
if legacy is None:
|
|
return None
|
|
|
|
migrated_profiles = deepcopy(legacy["profiles"])
|
|
_preserve_legacy_following_presets(migrated_profiles)
|
|
for profile in migrated_profiles.values():
|
|
for category in ("acceleration", "braking"):
|
|
config = profile[category]
|
|
if config["preset"] != "custom":
|
|
continue
|
|
legacy_curve = list(config["curve"])
|
|
minimum, maximum = CURVE_BOUNDS[category]
|
|
config["curve"] = [
|
|
round(min(max(_linear_interp(float(speed_mph), _V1_ACCELERATION_SPEEDS_MPH, config["curve"]), minimum), maximum), 4)
|
|
for speed_mph in ACCELERATION_SPEEDS_MPH
|
|
]
|
|
config["legacyCurve"] = legacy_curve
|
|
return strict_profile_document(profile_document(migrated_profiles, enabled=legacy["enabled"]))
|
|
|
|
|
|
def is_unconfigured_profile_document(raw_document) -> bool:
|
|
if raw_document is None:
|
|
return True
|
|
decoded = _decode_json(raw_document)
|
|
return isinstance(decoded, dict) and not decoded
|
|
|
|
|
|
def synchronise_profile_document_enabled(
|
|
raw_document, enabled: bool, ev_tuning: bool, truck_tuning: bool = False,
|
|
) -> dict | None:
|
|
if type(enabled) is not bool:
|
|
raise ValueError("enabled must be a JSON boolean")
|
|
document = migrate_profile_document(raw_document)
|
|
if document is None:
|
|
if not is_unconfigured_profile_document(raw_document) or not enabled:
|
|
return None
|
|
return profile_document(default_personality_profiles(ev_tuning, truck_tuning), enabled=True)
|
|
document["enabled"] = enabled
|
|
return strict_profile_document(document)
|
|
|
|
|
|
def strict_personality_profiles(raw_document) -> dict[str, dict] | None:
|
|
document = migrate_profile_document(raw_document)
|
|
if document is None or not document["enabled"]:
|
|
return None
|
|
return deepcopy(document["profiles"])
|
|
|
|
|
|
def load_personality_profiles(raw_document, ev_tuning: bool, truck_tuning: bool = False) -> dict[str, dict]:
|
|
document = migrate_profile_document(raw_document)
|
|
return deepcopy(document["profiles"]) if document is not None else default_personality_profiles(ev_tuning, truck_tuning)
|
|
|
|
|
|
def serialize_personality_profiles(profiles, ev_tuning: bool, truck_tuning: bool = False, *, enabled: bool) -> str:
|
|
del ev_tuning, truck_tuning
|
|
document = profile_document(profiles, enabled=enabled)
|
|
canonical = strict_profile_document(document)
|
|
if canonical is None:
|
|
raise ValueError("Longitudinal personality profiles must be complete and valid.")
|
|
return json.dumps(canonical, separators=(",", ":"), sort_keys=True, allow_nan=False)
|
|
|
|
|
|
def update_personality_profile(
|
|
profiles, personality: str, category: str, preset: str, curve, ev_tuning: bool, truck_tuning: bool = False,
|
|
*, reset: bool = False,
|
|
) -> dict[str, dict]:
|
|
if personality not in PERSONALITY_IDS:
|
|
raise ValueError(f"Unknown personality: {personality}")
|
|
if category not in _CATEGORY_SPECS:
|
|
raise ValueError(f"Unknown profile category: {category}")
|
|
base_document = profile_document(profiles, enabled=True)
|
|
canonical = strict_profile_document(base_document)
|
|
validated = _validated_category(category, {"preset": preset, "curve": curve})
|
|
if preset != "custom" and (curve != [] or reset):
|
|
validated = None
|
|
if validated is not None and preset == "custom" and not reset:
|
|
minimum, maximum = CURVE_BOUNDS[category]
|
|
previous = canonical["profiles"][personality][category] if canonical is not None else None
|
|
for index, value in enumerate(curve):
|
|
if not minimum <= value <= maximum and (
|
|
previous is None or not previous["curve"] or value != previous["curve"][index]
|
|
):
|
|
validated = None
|
|
break
|
|
if validated is None:
|
|
minimum, maximum = CURVE_BOUNDS[category]
|
|
presets, expected_length = _CATEGORY_SPECS[category]
|
|
message = f"Invalid {category} profile: preset must be one of {', '.join(presets)} and curve must contain "
|
|
message += f"{expected_length} finite numeric values between {minimum} and {maximum}."
|
|
raise ValueError(message)
|
|
|
|
if canonical is None:
|
|
base = default_personality_profiles(ev_tuning, truck_tuning)
|
|
else:
|
|
base = canonical["profiles"]
|
|
updated = deepcopy(base)
|
|
previous = updated[personality][category]
|
|
# A preset only changes which curve is active. Dormant Custom data, including
|
|
# preserved v1 runtime interpolation, survives switching and serialization.
|
|
if preset != "custom" or (not reset and validated["curve"] == previous["curve"]):
|
|
previous["preset"] = preset
|
|
return updated
|
|
updated[personality][category] = validated
|
|
return updated
|
|
|
|
|
|
def active_personality_id(traffic_mode: bool, personality) -> str | None:
|
|
if type(traffic_mode) is not bool:
|
|
return None
|
|
if traffic_mode:
|
|
return "traffic"
|
|
if isinstance(personality, bool):
|
|
return None
|
|
raw = getattr(personality, "raw", personality)
|
|
if isinstance(raw, bool) or not isinstance(raw, numbers.Integral):
|
|
return None
|
|
return {0: "aggressive", 1: "standard", 2: "relaxed"}.get(int(raw))
|
|
|
|
|
|
def resolve_personality_profile(raw_document, traffic_mode: bool, personality) -> dict | None:
|
|
profiles = strict_personality_profiles(raw_document)
|
|
personality_id = active_personality_id(traffic_mode, personality)
|
|
if profiles is None or personality_id is None:
|
|
return None
|
|
return deepcopy(profiles[personality_id])
|
|
|
|
|
|
def resolve_personality_category(raw_document, traffic_mode: bool, personality, category: str) -> dict | None:
|
|
profile = resolve_personality_profile(raw_document, traffic_mode, personality)
|
|
if profile is None or category not in _CATEGORY_SPECS:
|
|
return None
|
|
config = profile[category]
|
|
return None if config["preset"] == "dom_default" else deepcopy(config)
|
|
|
|
|
|
def category_curve(category: str, config: dict, ev_tuning: bool, truck_tuning: bool = False) -> list[float]:
|
|
validated = _validated_category(category, config)
|
|
if validated is None:
|
|
raise ValueError(f"Invalid {category} profile configuration.")
|
|
preset = validated["preset"]
|
|
if preset == "dom_default":
|
|
raise ValueError("Dom default resolves through the legacy controller path")
|
|
if preset == "custom":
|
|
return list(validated["curve"])
|
|
if category == "acceleration":
|
|
return _acceleration_preset_curve(preset, ev_tuning, truck_tuning)
|
|
if category == "braking":
|
|
return list(_BRAKING_PRESET_CURVES[preset])
|
|
return list(FOLLOWING_PRESET_CURVES[preset])
|
|
|
|
|
|
def _sample_config_on_custom_axis(
|
|
category: str, config: dict, ev_tuning: bool, truck_tuning: bool,
|
|
) -> list[float]:
|
|
return [
|
|
round(interpolate_category_curve(category, speed_mph * 0.44704, config, ev_tuning, truck_tuning), 4)
|
|
for speed_mph in _CATEGORY_SPEEDS_MPH[category]
|
|
]
|
|
|
|
|
|
def personality_reference_curves(ev_tuning: bool, truck_tuning: bool = False) -> dict[str, dict[str, list[float]]]:
|
|
return {
|
|
personality: {
|
|
category: _sample_config_on_custom_axis(
|
|
category,
|
|
{"preset": preset, "curve": []},
|
|
ev_tuning,
|
|
truck_tuning,
|
|
)
|
|
for category, preset in presets.items()
|
|
}
|
|
for personality, presets in _PERSONALITY_REFERENCE_PRESETS.items()
|
|
}
|
|
|
|
|
|
def initial_custom_curve(
|
|
category: str,
|
|
current_config: dict,
|
|
ev_tuning: bool,
|
|
truck_tuning: bool,
|
|
*,
|
|
legacy_curve: list[float] | None = None,
|
|
) -> list[float]:
|
|
if category not in _CATEGORY_SPECS or not isinstance(current_config, dict):
|
|
raise ValueError("Unknown or malformed profile category")
|
|
preset = current_config.get("preset")
|
|
if current_config.get("curve"):
|
|
candidate = current_config["curve"]
|
|
elif preset == "dom_default":
|
|
candidate = legacy_curve
|
|
if category in ("acceleration", "braking") and isinstance(candidate, list) and len(candidate) == len(_V1_ACCELERATION_SPEEDS_MPH):
|
|
candidate = [
|
|
round(_linear_interp(float(speed_mph), _V1_ACCELERATION_SPEEDS_MPH, candidate), 4)
|
|
for speed_mph in _CATEGORY_SPEEDS_MPH[category]
|
|
]
|
|
elif preset == "custom":
|
|
candidate = current_config.get("curve")
|
|
elif isinstance(preset, str):
|
|
candidate = _sample_config_on_custom_axis(category, {"preset": preset, "curve": []}, ev_tuning, truck_tuning)
|
|
minimum, maximum = CURVE_BOUNDS[category]
|
|
candidate = [round(min(max(value, minimum), maximum), 4) for value in candidate]
|
|
else:
|
|
candidate = None
|
|
validated = _validated_category(category, {"preset": "custom", "curve": candidate})
|
|
if validated is None:
|
|
raise ValueError(f"Cannot initialize Custom {category} from the current selection")
|
|
return validated["curve"]
|
|
|
|
|
|
def _linear_interp(value: float, breakpoints: tuple[float, ...], values: list[float]) -> float:
|
|
if value <= breakpoints[0]:
|
|
return float(values[0])
|
|
if value >= breakpoints[-1]:
|
|
return float(values[-1])
|
|
index = next(index for index, point in enumerate(breakpoints[1:], start=1) if point >= value) - 1
|
|
t = (value - breakpoints[index]) / float(breakpoints[index + 1] - breakpoints[index])
|
|
return float(values[index] + t * (values[index + 1] - values[index]))
|
|
|
|
|
|
def interpolate_category_curve(
|
|
category: str, v_ego: float, config: dict, ev_tuning: bool, truck_tuning: bool = False,
|
|
) -> float:
|
|
if not isinstance(v_ego, numbers.Real) or isinstance(v_ego, bool) or not math.isfinite(float(v_ego)):
|
|
raise ValueError("Vehicle speed must be finite")
|
|
validated = _validated_category(category, config)
|
|
if validated is None:
|
|
raise ValueError(f"Invalid {category} profile configuration.")
|
|
values = category_curve(category, validated, ev_tuning, truck_tuning)
|
|
if validated["preset"] == "custom" and "legacyCurve" in validated:
|
|
return _linear_interp(float(v_ego), _NATIVE_ACCELERATION_SPEEDS_MS, validated["legacyCurve"])
|
|
if category == "acceleration":
|
|
from openpilot.starpilot.common.accel_profile import interpolate_accel_profile
|
|
breakpoints = _NATIVE_ACCELERATION_SPEEDS_MS if validated["preset"] != "custom" else tuple(
|
|
speed * 0.44704 for speed in ACCELERATION_SPEEDS_MPH
|
|
)
|
|
return interpolate_accel_profile(float(v_ego), values, breakpoints)
|
|
breakpoints = (_FOLLOWING_PRESET_SPEEDS_MPH[validated["preset"]]
|
|
if category == "following" and validated["preset"] != "custom" else _CATEGORY_SPEEDS_MPH[category])
|
|
return _linear_interp(float(v_ego) / 0.44704, breakpoints, values)
|