mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-15 20:13:49 +08:00
Align following presets with Dom defaults
This commit is contained in:
committed by
firestar5683
parent
ab6c97fef8
commit
6f3d863ecd
@@ -24,7 +24,42 @@ Dom-default points outside the ordinary editor range (such as Traffic braking
|
||||
at 0.35 m/s², configured Traffic following at 0.5 seconds or truck acceleration
|
||||
at 6 m/s²) remain visible and are preserved when another point is edited.
|
||||
New point edits still use the existing authoring bounds. This does not expand
|
||||
braking authority or change named-preset controller behaviour.
|
||||
braking authority or change acceleration/braking preset definitions.
|
||||
|
||||
## Following presets
|
||||
|
||||
Named following presets now match Dom's factory following settings with custom
|
||||
personalities enabled. Close follows Aggressive, Medium follows Standard and
|
||||
Far follows Relaxed. The presets are available in every personality.
|
||||
|
||||
| Preset | Previous curve | Revised curve |
|
||||
| --- | --- | --- |
|
||||
| Close | 1.25 s at every speed | 1.25 s through 45 mph, falling to 1.0 s at 70 mph |
|
||||
| Medium | 1.45 s at every speed | 1.45 s through 45 mph, falling to 1.2 s at 70 mph |
|
||||
| Far | 1.75 s at every speed | 1.6 s through 45 mph, falling to 1.4 s at 70 mph |
|
||||
| Traffic | No named preset | 0.75 s at rest, rising to 1.6 s at 25 m/s (55.92 mph) |
|
||||
|
||||
Interpolation is linear between the stated breakpoints and constant outside
|
||||
them. Named presets use the exact native speed axes at runtime. First-use
|
||||
Custom conversion samples them onto the existing 10 mph editor grid.
|
||||
|
||||
Existing v1/v2 Close, Medium and Far selections keep their old fixed headways
|
||||
as `legacy_close`, `legacy_medium` and `legacy_far`. Both Galaxy pickers show
|
||||
the selected compatibility entry as **Previous Close**, **Previous Medium**
|
||||
or **Previous Far**. Explicitly selecting a current preset adopts its new curve.
|
||||
The previous entry disappears when it is no longer selected.
|
||||
|
||||
Existing `dom_default` selections continue to inherit configured settings;
|
||||
they are not silently converted to fixed named presets. Fresh profiles also
|
||||
retain this inheritance. The named curves match untouched factory settings;
|
||||
users' changed global following values can still differ from them.
|
||||
|
||||
Acceleration and braking presets are unchanged. Standard acceleration and Eco
|
||||
braking match the normal factory defaults for Aggressive, Standard and Relaxed
|
||||
when named-preset and global powertrain tuning agree. Named presets use detected
|
||||
EV/truck tuning; the Dom-default resolver respects the global tuning switches,
|
||||
so these can differ. Traffic retains its dedicated acceleration/braking defaults;
|
||||
this change adds only its named following preset.
|
||||
|
||||
## Storage compatibility
|
||||
|
||||
@@ -34,7 +69,8 @@ only Custom uses them. An actual graph edit or reset retires preserved v1
|
||||
interpolation for that category; a preset switch or unchanged submission does
|
||||
not.
|
||||
|
||||
Valid v2 documents are read losslessly and upgraded on the next normal write.
|
||||
Valid v2 documents retain their runtime meaning and are upgraded on the next
|
||||
normal write, including the fixed following compatibility names above.
|
||||
Version 1 keeps its existing explicit, verified migration flow. Reads never
|
||||
rewrite Params. Category conflict detection, off-road checks and atomic profile
|
||||
document writes still apply to edits and resets.
|
||||
|
||||
@@ -36,7 +36,8 @@ def is_truck_fingerprint(fingerprint: object) -> bool:
|
||||
|
||||
ACCELERATION_PRESETS = ("dom_default", "standard", "eco", "sport", "sport_plus", "custom")
|
||||
BRAKING_PRESETS = ("dom_default", "standard", "eco", "sport", "custom")
|
||||
FOLLOWING_PRESETS = ("dom_default", "close", "medium", "far", "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),
|
||||
@@ -115,10 +116,21 @@ _BRAKING_PRESET_CURVES = {
|
||||
"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,) * len(FOLLOWING_SPEEDS_MPH),
|
||||
"medium": (1.45,) * len(FOLLOWING_SPEEDS_MPH),
|
||||
"far": (1.75,) * len(FOLLOWING_SPEEDS_MPH),
|
||||
"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": {
|
||||
@@ -148,7 +160,7 @@ _ACCELERATION_PROFILE_IDS = {
|
||||
}
|
||||
|
||||
_PERSONALITY_REFERENCE_PRESETS = {
|
||||
"traffic": {"acceleration": "eco", "braking": "standard", "following": "close"},
|
||||
"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"},
|
||||
@@ -309,6 +321,10 @@ def _strict_document(
|
||||
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,
|
||||
@@ -325,9 +341,16 @@ def _strict_document(
|
||||
}
|
||||
|
||||
|
||||
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:
|
||||
# Version 2 has the same axes and active curves. Read it losslessly; the next
|
||||
# normal save upgrades the document without requiring a destructive reset.
|
||||
# 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):
|
||||
@@ -341,6 +364,8 @@ def strict_profile_document(raw_document) -> dict | None:
|
||||
_V1_CURVE_BOUNDS,
|
||||
)
|
||||
if document is not None:
|
||||
if version == 2:
|
||||
_preserve_legacy_following_presets(document["profiles"])
|
||||
document["schemaVersion"] = PROFILE_SCHEMA_VERSION
|
||||
return document
|
||||
|
||||
@@ -361,6 +386,7 @@ def migrate_profile_document(raw_document) -> dict | 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]
|
||||
@@ -592,4 +618,6 @@ def interpolate_category_curve(
|
||||
speed * 0.44704 for speed in ACCELERATION_SPEEDS_MPH
|
||||
)
|
||||
return interpolate_accel_profile(float(v_ego), values, breakpoints)
|
||||
return _linear_interp(float(v_ego) / 0.44704, _CATEGORY_SPEEDS_MPH[category], values)
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
from copy import deepcopy
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from openpilot.starpilot.common import longitudinal_personality_profiles as lpp
|
||||
|
||||
|
||||
@pytest.mark.parametrize("preset,breakpoints,values", [
|
||||
("close", [45.0, 70.0], [1.25, 1.0]),
|
||||
("medium", [45.0, 70.0], [1.45, 1.2]),
|
||||
("far", [45.0, 70.0], [1.6, 1.4]),
|
||||
("traffic", [0.0, 25.0 / 0.44704], [0.75, 1.6]),
|
||||
])
|
||||
def test_named_following_presets_match_dom_at_and_between_native_breakpoints(preset, breakpoints, values):
|
||||
config = {"preset": preset, "curve": []}
|
||||
for mph in sorted(set([0, 10, 44.9, 45, 45.1, 50, 55, 55.9234073, 60, 65, 69.9, 70, 70.1, 90, 120, *breakpoints])):
|
||||
expected = float(np.interp(mph, breakpoints, values))
|
||||
assert lpp.interpolate_category_curve("following", mph * 0.44704, config, False) == pytest.approx(expected, abs=1e-12)
|
||||
sampled = lpp.initial_custom_curve("following", config, False, False)
|
||||
assert sampled == [round(float(np.interp(mph, breakpoints, values)), 4) for mph in lpp.FOLLOWING_SPEEDS_MPH]
|
||||
|
||||
|
||||
def test_close_medium_far_keep_their_order_at_every_speed():
|
||||
for mph in np.linspace(0, 120, 481):
|
||||
values = [lpp.interpolate_category_curve("following", mph * 0.44704, {"preset": p, "curve": []}, False) for p in ("close", "medium", "far")]
|
||||
assert values[0] < values[1] < values[2]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("preset,value", [("close", 1.25), ("medium", 1.45), ("far", 1.75)])
|
||||
def test_old_named_selection_keeps_fixed_distance_after_load_save_and_reload(preset, value):
|
||||
original = lpp.profile_document(lpp.default_personality_profiles(False), enabled=True)
|
||||
original["schemaVersion"] = 2
|
||||
original["profiles"]["standard"]["following"] = {"preset": preset, "curve": []}
|
||||
saved = deepcopy(original)
|
||||
loaded = lpp.strict_profile_document(original)
|
||||
assert original == saved
|
||||
config = loaded["profiles"]["standard"]["following"]
|
||||
assert config == {"preset": "legacy_" + preset, "curve": []}
|
||||
for mph in (0, 45, 60, 70, 100):
|
||||
assert lpp.interpolate_category_curve("following", mph * 0.44704, config, False) == value
|
||||
reloaded = lpp.strict_profile_document(lpp.serialize_personality_profiles(loaded["profiles"], False, enabled=True))
|
||||
assert reloaded == loaded
|
||||
updated = lpp.update_personality_profile(loaded["profiles"], "standard", "following", preset, [], False)
|
||||
assert updated["standard"]["following"] == {"preset": preset, "curve": []}
|
||||
assert lpp.interpolate_category_curve("following", 70 * 0.44704, updated["standard"]["following"], False) != value
|
||||
|
||||
|
||||
@pytest.mark.parametrize("preset", ["traffic", "legacy_close", "legacy_medium", "legacy_far"])
|
||||
def test_old_schema_cannot_smuggle_new_preset_names(preset):
|
||||
document = lpp.profile_document(lpp.default_personality_profiles(False), enabled=True)
|
||||
document["schemaVersion"] = 2
|
||||
document["profiles"]["standard"]["following"]["preset"] = preset
|
||||
assert lpp.strict_profile_document(document) is None
|
||||
|
||||
|
||||
def test_named_following_switches_keep_remembered_custom_points():
|
||||
profiles = lpp.default_personality_profiles(False)
|
||||
custom = [1.55] * 10
|
||||
profiles["traffic"]["following"] = {"preset": "custom", "curve": custom}
|
||||
for preset in ("close", "medium", "far", "traffic", "dom_default"):
|
||||
profiles = lpp.update_personality_profile(profiles, "traffic", "following", preset, [], False)
|
||||
assert profiles["traffic"]["following"]["curve"] == custom
|
||||
restored = lpp.initial_custom_curve("following", profiles["traffic"]["following"], False, False)
|
||||
assert restored == custom
|
||||
@@ -474,9 +474,13 @@ def test_fresh_profiles_start_with_dom_defaults():
|
||||
|
||||
def test_following_presets_match_stock_dom_personalities_exactly():
|
||||
assert FOLLOWING_PRESET_CURVES == {
|
||||
"close": (1.25,) * 10,
|
||||
"medium": (1.45,) * 10,
|
||||
"far": (1.75,) * 10,
|
||||
"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),
|
||||
}
|
||||
|
||||
|
||||
@@ -500,9 +504,9 @@ def test_named_acceleration_presets_keep_native_dom_interpolation():
|
||||
def test_reference_curves_are_profile_specific_and_use_the_custom_axis():
|
||||
references = lpp.personality_reference_curves(False, False)
|
||||
assert references["traffic"]["acceleration"] != references["aggressive"]["acceleration"]
|
||||
assert references["aggressive"]["following"] == [1.25] * 10
|
||||
assert references["standard"]["following"] == [1.45] * 10
|
||||
assert references["relaxed"]["following"] == [1.75] * 10
|
||||
assert references["aggressive"]["following"] == [1.25] * 5 + [1.2, 1.1, 1.0, 1.0, 1.0]
|
||||
assert references["standard"]["following"] == [1.45] * 5 + [1.4, 1.3, 1.2, 1.2, 1.2]
|
||||
assert references["relaxed"]["following"] == [1.6] * 5 + [1.56, 1.48, 1.4, 1.4, 1.4]
|
||||
for profile in references.values():
|
||||
for curve in profile.values():
|
||||
assert len(curve) == 10
|
||||
@@ -545,7 +549,10 @@ def test_exact_v1_document_migrates_whole_or_not_at_all():
|
||||
assert max(migrated_acceleration["curve"]) == CURVE_BOUNDS["acceleration"][1]
|
||||
assert migrated_acceleration["legacyCurve"] == legacy_profiles["aggressive"]["acceleration"]["curve"]
|
||||
assert interpolate_category_curve("acceleration", 40.0, migrated_acceleration, False, False) == 4.0
|
||||
assert migrated["profiles"]["standard"] == legacy_profiles["standard"]
|
||||
assert migrated["profiles"]["standard"] == {
|
||||
**legacy_profiles["standard"],
|
||||
"following": {"preset": "legacy_medium", "curve": []},
|
||||
}
|
||||
|
||||
malformed = json.loads(json.dumps(legacy))
|
||||
malformed["profiles"]["aggressive"]["acceleration"]["curve"][0] = True
|
||||
|
||||
@@ -213,7 +213,7 @@ def test_traffic_profile_wins_over_cereal_personality_without_changing_jerk():
|
||||
|
||||
controller.update(True, 0.0, _sm(traffic=True, personality=Personality.aggressive), _toggles(document))
|
||||
|
||||
assert controller.t_follow == pytest.approx(1.75)
|
||||
assert controller.t_follow == pytest.approx(1.6)
|
||||
assert controller.base_acceleration_jerk == 1.0
|
||||
|
||||
|
||||
@@ -347,3 +347,29 @@ def test_every_advanced_param_maps_to_runtime_attribute_with_hundredth_conversio
|
||||
assert keywords["conversion"] == 0.01
|
||||
assert keywords["min"] == 0.25
|
||||
assert keywords["max"] == 2.0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("profile,personality,preset", [
|
||||
("aggressive", Personality.aggressive, "close"),
|
||||
("standard", Personality.standard, "medium"),
|
||||
("relaxed", Personality.relaxed, "far"),
|
||||
("traffic", Personality.standard, "traffic"),
|
||||
])
|
||||
def test_named_presets_match_dom_default_runtime_with_factory_following_settings(monkeypatch, profile, personality, preset):
|
||||
# Dom CITY_SPEED_LIMIT is 25 m/s, not 25 mph as the older fixture assumes.
|
||||
monkeypatch.setattr(following_module, "TRAFFIC_MODE_BP", [0.0, 25.0])
|
||||
default_document = _document()
|
||||
named_document = _document()
|
||||
named_document["profiles"][profile]["following"] = {"preset": preset, "curve": []}
|
||||
for speed in (0, 5, 10, 20, 45 * 0.44704, 50 * 0.44704, 25, 60 * 0.44704, 70 * 0.44704, 40):
|
||||
results = []
|
||||
for document in (default_document, named_document):
|
||||
toggles = _toggles(document)
|
||||
toggles.aggressive_follow = [1.25, 1.0]
|
||||
toggles.standard_follow = [1.45, 1.2]
|
||||
toggles.relaxed_follow = [1.6, 1.4]
|
||||
toggles.traffic_mode_follow = [0.75, 1.6]
|
||||
controller = StarPilotFollowing(_planner())
|
||||
controller.update(True, speed, _sm(personality=personality, traffic=profile == "traffic"), toggles)
|
||||
results.append((controller.t_follow, controller.base_acceleration_jerk, controller.base_danger_jerk, controller.base_speed_jerk))
|
||||
assert results[0] == pytest.approx(results[1], abs=1e-12)
|
||||
|
||||
@@ -116,7 +116,7 @@ const PERSONALITY_CATEGORY_DEFINITIONS = {
|
||||
const PERSONALITY_OPTION_ORDER = {
|
||||
acceleration: ["eco", "standard", "sport", "sport_plus", "custom"],
|
||||
braking: ["eco", "standard", "sport", "custom"],
|
||||
following: ["close", "medium", "far", "custom"],
|
||||
following: ["close", "medium", "far", "traffic", "custom"],
|
||||
}
|
||||
const PERSONALITY_ADVANCED_KEYS = {
|
||||
traffic: ["TrafficJerkAcceleration", "TrafficJerkDeceleration", "TrafficJerkDanger", "TrafficJerkSpeedDecrease", "TrafficJerkSpeed"],
|
||||
@@ -1667,7 +1667,7 @@ function handleSectionTabClick(sectionSlug, event) {
|
||||
}
|
||||
|
||||
function personalityPresetLabel(preset) {
|
||||
return String(preset || "").split("_").map(part => part === "plus" ? "+" : `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join(" ").replace(" +", "+")
|
||||
return String(preset || "").split("_").map(part => part === "plus" ? "+" : part === "legacy" ? "Previous" : `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join(" ").replace(" +", "+")
|
||||
}
|
||||
|
||||
function personalityUpdateKey(profileId, category) {
|
||||
@@ -2122,6 +2122,7 @@ function renderPersonalityCategoryField(profile, category, config) {
|
||||
const definition = PERSONALITY_CATEGORY_DEFINITIONS[category]
|
||||
const availableOptions = new Set((state.personalityMeta?.options?.[category] || []).filter(option => option !== "dom_default"))
|
||||
const options = (PERSONALITY_OPTION_ORDER[category] || []).filter(option => availableOptions.has(option))
|
||||
if (category === "following" && config.preset.startsWith("legacy_") && availableOptions.has(config.preset)) options.unshift(config.preset)
|
||||
const updateKey = personalityUpdateKey(profile.id, category)
|
||||
return html`
|
||||
<section class="ds-personality-field" aria-labelledby="personality-field-${profile.id}-${category}">
|
||||
@@ -2139,6 +2140,7 @@ function renderPersonalityCategoryField(profile, category, config) {
|
||||
`)}
|
||||
</div>
|
||||
${() => config.preset === "dom_default" ? html`<small class="ds-personality-default-note">Using existing StarPilot defaults.</small>` : ""}
|
||||
${() => category === "following" && config.preset.startsWith("legacy_") ? html`<small class="ds-personality-default-note">Previous fixed following distance retained. Select a preset to use its current curve.</small>` : ""}
|
||||
</section>
|
||||
`
|
||||
}
|
||||
|
||||
@@ -29,12 +29,17 @@ export const PersonalityProfiles = {
|
||||
},
|
||||
methods: {
|
||||
enabled(value) { return [true, 1, "1", "True", "true"].includes(value) },
|
||||
label(value) { return value.split("_").map(s => s === "plus" ? "+" : s[0].toUpperCase() + s.slice(1)).join(" ").replace(" +", "+") },
|
||||
label(value) { return value.split("_").map(s => s === "plus" ? "+" : s === "legacy" ? "Previous" : s[0].toUpperCase() + s.slice(1)).join(" ").replace(" +", "+") },
|
||||
key: personalityProfileParamKey,
|
||||
speed(value) { return formatProfileSpeed(value, this.enabled(this.values.IsMetric)) },
|
||||
speedUnit() { return profileSpeedUnit(this.enabled(this.values.IsMetric)) },
|
||||
bounds(param) { return numericBounds(param, this.values) },
|
||||
options(category) { return (category === "following" ? ["close", "medium", "far", "custom"] : ["eco", "standard", "sport", "sport_plus", "custom"]).filter(x => this.data.options[category].includes(x)) },
|
||||
options(category, profile) {
|
||||
const choices = (category === "following" ? ["close", "medium", "far", "traffic", "custom"] : ["eco", "standard", "sport", "sport_plus", "custom"]).filter(x => this.data.options[category].includes(x))
|
||||
const selected = this.data.profiles[profile]?.[category]?.preset
|
||||
if (category === "following" && selected?.startsWith("legacy_") && this.data.options[category].includes(selected)) choices.unshift(selected)
|
||||
return choices
|
||||
},
|
||||
advancedParams(profile) {
|
||||
return Object.values(this.meta).filter(p => p.parent_key === this.key(profile) && p.key.includes("Jerk"))
|
||||
},
|
||||
@@ -288,11 +293,12 @@ export const PersonalityProfiles = {
|
||||
<section v-for="(title, category) in CATEGORIES" :key="category" class="gx-personalities__category">
|
||||
<h4>{{ title }}</h4>
|
||||
<div class="gx-personalities__options" role="group" :aria-label="label(profile) + ' ' + title">
|
||||
<button v-for="option in options(category)" :key="option" type="button" class="gx-btn gx-btn--tonal"
|
||||
<button v-for="option in options(category, profile)" :key="option" type="button" class="gx-btn gx-btn--tonal"
|
||||
:aria-pressed="data.profiles[profile][category].preset === option" :disabled="editingLocked"
|
||||
@click="preset(profile, category, option)">{{ label(option) }}</button>
|
||||
</div>
|
||||
<p v-if="data.profiles[profile][category].preset === 'dom_default'">Using existing Dom default.</p>
|
||||
<p v-if="category === 'following' && data.profiles[profile][category].preset.startsWith('legacy_')">Previous fixed following distance retained. Select a preset to use its current curve.</p>
|
||||
</section>
|
||||
<details class="gx-personalities__advanced" :open="advancedOpen[profile]" @toggle="advancedOpen[profile] = $event.target.open">
|
||||
<summary>Advanced</summary>
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"profiles": {"traffic": {"acceleration": {"preset": "standard", "curve": []}, "braking": {"preset": "standard", "curve": []}, "following": {"preset": "medium", "curve": []}}, "aggressive": {"acceleration": {"preset": "standard", "curve": []}, "braking": {"preset": "standard", "curve": []}, "following": {"preset": "medium", "curve": []}}, "standard": {"acceleration": {"preset": "standard", "curve": []}, "braking": {"preset": "standard", "curve": []}, "following": {"preset": "medium", "curve": []}}, "relaxed": {"acceleration": {"preset": "standard", "curve": []}, "braking": {"preset": "standard", "curve": []}, "following": {"preset": "medium", "curve": []}}}, "reference_curves": {"traffic": {"acceleration": [1.5, 1.302, 1.1135, 0.9375, 0.8039, 0.6611, 0.547, 0.4797, 0.3781, 0.35], "braking": [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], "following": [1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25]}, "aggressive": {"acceleration": [3.5, 3.203, 2.827, 2.4343, 2.0616, 1.7444, 1.5441, 1.4093, 1.2063, 1.15], "braking": [2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], "following": [1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25]}, "standard": {"acceleration": [2.0, 1.802, 1.5669, 1.3468, 1.1398, 0.9611, 0.8456, 0.7445, 0.5922, 0.55], "braking": [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], "following": [1.45, 1.45, 1.45, 1.45, 1.45, 1.45, 1.45, 1.45, 1.45, 1.45]}, "relaxed": {"acceleration": [1.5, 1.302, 1.1135, 0.9375, 0.8039, 0.6611, 0.547, 0.4797, 0.3781, 0.35], "braking": [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5], "following": [1.75, 1.75, 1.75, 1.75, 1.75, 1.75, 1.75, 1.75, 1.75, 1.75]}}, "bounds": {"acceleration": [0.0, 3.5], "braking": [0.5, 2.0], "following": [0.75, 3.0]}, "options": {"acceleration": ["dom_default", "standard", "eco", "sport", "sport_plus", "custom"], "braking": ["dom_default", "standard", "eco", "sport", "custom"], "following": ["dom_default", "close", "medium", "far", "custom"]}, "speed_breakpoints_mph": {"acceleration": [0, 10, 20, 30, 40, 50, 60, 70, 80, 90], "braking": [0, 10, 20, 30, 40, 50, 60, 70, 80, 90], "following": [0, 10, 20, 30, 40, 50, 60, 70, 80, 90]}, "migration_required": false}
|
||||
{"profiles": {"traffic": {"acceleration": {"preset": "standard", "curve": []}, "braking": {"preset": "standard", "curve": []}, "following": {"preset": "medium", "curve": []}}, "aggressive": {"acceleration": {"preset": "standard", "curve": []}, "braking": {"preset": "standard", "curve": []}, "following": {"preset": "medium", "curve": []}}, "standard": {"acceleration": {"preset": "standard", "curve": []}, "braking": {"preset": "standard", "curve": []}, "following": {"preset": "medium", "curve": []}}, "relaxed": {"acceleration": {"preset": "standard", "curve": []}, "braking": {"preset": "standard", "curve": []}, "following": {"preset": "medium", "curve": []}}}, "reference_curves": {"traffic": {"acceleration": [1.5, 1.302, 1.1135, 0.9375, 0.8039, 0.6611, 0.547, 0.4797, 0.3781, 0.35], "braking": [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], "following": [1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25]}, "aggressive": {"acceleration": [3.5, 3.203, 2.827, 2.4343, 2.0616, 1.7444, 1.5441, 1.4093, 1.2063, 1.15], "braking": [2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], "following": [1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25]}, "standard": {"acceleration": [2.0, 1.802, 1.5669, 1.3468, 1.1398, 0.9611, 0.8456, 0.7445, 0.5922, 0.55], "braking": [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], "following": [1.45, 1.45, 1.45, 1.45, 1.45, 1.45, 1.45, 1.45, 1.45, 1.45]}, "relaxed": {"acceleration": [1.5, 1.302, 1.1135, 0.9375, 0.8039, 0.6611, 0.547, 0.4797, 0.3781, 0.35], "braking": [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5], "following": [1.75, 1.75, 1.75, 1.75, 1.75, 1.75, 1.75, 1.75, 1.75, 1.75]}}, "bounds": {"acceleration": [0.0, 3.5], "braking": [0.5, 2.0], "following": [0.75, 3.0]}, "options": {"acceleration": ["dom_default", "standard", "eco", "sport", "sport_plus", "custom"], "braking": ["dom_default", "standard", "eco", "sport", "custom"], "following": ["dom_default", "close", "medium", "far", "traffic", "custom", "legacy_close", "legacy_medium", "legacy_far"]}, "speed_breakpoints_mph": {"acceleration": [0, 10, 20, 30, 40, 50, 60, 70, 80, 90], "braking": [0, 10, 20, 30, 40, 50, 60, 70, 80, 90], "following": [0, 10, 20, 30, 40, 50, 60, 70, 80, 90]}, "migration_required": false}
|
||||
@@ -0,0 +1,43 @@
|
||||
const assert = require('assert');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = async ({page, data, writes, errors, output}) => {
|
||||
const profiles = ['traffic', 'aggressive', 'standard', 'relaxed'];
|
||||
const legacy = ['legacy_close', 'legacy_medium', 'legacy_far'];
|
||||
const custom = Array(10).fill(1.55);
|
||||
const settled = () => page.waitForFunction(() => {
|
||||
const vm = document.querySelector('#app').__vue_app__._instance.proxy;
|
||||
return !vm.busy && !vm.curvePending && vm.ready;
|
||||
});
|
||||
for (let i = 0; i < profiles.length; i++) {
|
||||
data.profiles[profiles[i]].following = {preset: legacy[i % 3], curve: [...custom]};
|
||||
}
|
||||
await page.reload();
|
||||
await page.getByRole('button', {name: 'Manage', exact: true}).click();
|
||||
for (let i = 0; i < profiles.length; i++) {
|
||||
const profile = page.locator('.gx-personalities__profile').nth(i);
|
||||
const section = profile.locator('.gx-personalities__category').nth(2);
|
||||
const previousName = 'Previous ' + ['Close', 'Medium', 'Far'][i % 3];
|
||||
const previous = section.getByRole('button', {name: previousName, exact: true});
|
||||
assert.equal(await previous.getAttribute('aria-pressed'), 'true');
|
||||
assert(await section.getByText('Previous fixed following distance retained.', {exact: false}).isVisible());
|
||||
for (const preset of ['Traffic', 'Close', 'Medium', 'Far', 'Custom']) {
|
||||
await section.getByRole('button', {name: preset, exact: true}).click();
|
||||
await settled();
|
||||
assert.equal(writes.at(-1).preset, preset.toLowerCase());
|
||||
assert.deepEqual(data.profiles[profiles[i]].following.curve, custom);
|
||||
assert.equal(await previous.count(), 0);
|
||||
assert.equal(await section.getByRole('button', {name: preset, exact: true}).getAttribute('aria-pressed'), 'true');
|
||||
}
|
||||
}
|
||||
await page.reload();
|
||||
await page.getByRole('button', {name: 'Manage', exact: true}).click();
|
||||
for (const profile of profiles) assert.deepEqual(data.profiles[profile].following, {preset: 'custom', curve: custom});
|
||||
for (const width of [320, 1280]) {
|
||||
await page.setViewportSize({width, height: 1000});
|
||||
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
|
||||
await page.screenshot({path: path.join(output, `following-presets-${width}.png`), fullPage: true});
|
||||
}
|
||||
assert.deepEqual(errors, []);
|
||||
console.log('PASS: Previous Close/Medium/Far retained and selected; explicit new Traffic/Close/Medium/Far selection; dormant Custom restoration in all four profiles; reload and responsive layout');
|
||||
};
|
||||
@@ -40,6 +40,10 @@ const output=process.env.PERSONALITY_BROWSER_OUTPUT || path.join(require('os').t
|
||||
assert(!(await page.locator('#gx-personality-settings').isVisible()));
|
||||
await page.getByRole('button',{name:'Manage',exact:true}).click();
|
||||
assert.equal(await page.locator('.gx-personalities__profile').count(),4);
|
||||
if(process.env.PERSONALITY_FOLLOWING_ONLY){
|
||||
await require('./personality_following_presets.cjs')({page,data,writes,errors,output});
|
||||
return;
|
||||
}
|
||||
if(process.env.PERSONALITY_CUSTOM_ONLY){
|
||||
await require('./personality_custom_graphs.cjs')({page,data,values,writes,errors,output});
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
from copy import deepcopy
|
||||
|
||||
import pytest
|
||||
|
||||
from openpilot.starpilot.common import longitudinal_personality_profiles as lpp
|
||||
from test_personality_profiles_api import _client
|
||||
|
||||
|
||||
@pytest.mark.parametrize("personality,preset", [("aggressive", "close"), ("standard", "medium"), ("relaxed", "far"), ("traffic", "traffic")])
|
||||
def test_named_following_matches_default_reference_and_custom_conversion(monkeypatch, personality, preset):
|
||||
client, _ = _client(monkeypatch, {"CustomPersonalities": True})
|
||||
endpoint = "/api/personality_profiles"
|
||||
response = client.put(endpoint, json={"profile": personality, "category": "following", "preset": preset, "curve": []})
|
||||
assert response.status_code == 200
|
||||
body = response.get_json()
|
||||
assert preset in body["options"]["following"]
|
||||
reference = body["reference_curves"][personality]["following"]
|
||||
assert [round(lpp.interpolate_category_curve("following", speed * 0.44704, body["profiles"][personality]["following"], False), 4) for speed in lpp.FOLLOWING_SPEEDS_MPH] == reference
|
||||
response = client.put(endpoint, json={"profile": personality, "category": "following", "preset": "custom", "curve": []})
|
||||
assert response.status_code == 200
|
||||
assert response.get_json()["profiles"][personality]["following"]["curve"] == reference
|
||||
|
||||
|
||||
def test_existing_fixed_selection_stays_visible_and_can_explicitly_change(monkeypatch):
|
||||
profiles = lpp.default_personality_profiles(False)
|
||||
profiles["standard"]["following"] = {"preset": "medium", "curve": []}
|
||||
document = lpp.profile_document(profiles, enabled=True)
|
||||
document["schemaVersion"] = 2
|
||||
original = deepcopy(document)
|
||||
client, params = _client(monkeypatch, {"CustomPersonalities": True, lpp.PERSONALITY_PROFILES_PARAM: document})
|
||||
body = client.get("/api/personality_profiles").get_json()
|
||||
assert body["migration_required"] is False
|
||||
assert body["profiles"]["standard"]["following"]["preset"] == "legacy_medium"
|
||||
assert "legacy_medium" in body["options"]["following"]
|
||||
assert params.values[lpp.PERSONALITY_PROFILES_PARAM] == original
|
||||
assert params.writes == []
|
||||
# The comparison includes the upgraded legacy identity, protecting old data.
|
||||
response = client.put("/api/personality_profiles", json={"profile": "standard", "category": "following", "preset": "medium", "curve": [], "expected": body["profiles"]["standard"]["following"]})
|
||||
assert response.status_code == 200
|
||||
assert response.get_json()["profiles"]["standard"]["following"]["preset"] == "medium"
|
||||
|
||||
|
||||
def test_existing_dom_default_keeps_inheriting_user_following_settings(monkeypatch):
|
||||
client, _ = _client(monkeypatch, {"CustomPersonalities": True, "StandardFollow": 1.8, "StandardFollowHigh": 1.6})
|
||||
body = client.get("/api/personality_profiles").get_json()
|
||||
assert body["profiles"]["standard"]["following"]["preset"] == "dom_default"
|
||||
assert body["reference_curves"]["standard"]["following"][0] == 1.8
|
||||
assert body["reference_curves"]["standard"]["following"][-1] == 1.6
|
||||
@@ -658,3 +658,35 @@ def test_responsive_canvas_keeps_metric_endpoint_labels_separate_and_scales_bitm
|
||||
assert [labels[0]["text"], labels[-1]["text"]] == ["0", "144.8"]
|
||||
for left, right in zip(labels, labels[1:]):
|
||||
assert left["x"] + left["width"] / 2 + 6 <= right["x"] - right["width"] / 2
|
||||
|
||||
|
||||
def test_classic_following_picker_displays_migrated_selection_and_current_traffic_option():
|
||||
source = DEVICE_SETTINGS_PATH.read_text(encoding="utf-8")
|
||||
functions = "\n".join(
|
||||
"function " + name + source.split("function " + name, 1)[1].split("\n}\n", 1)[0] + "\n}"
|
||||
for name in ("personalityPresetLabel", "personalityUpdateKey", "renderPersonalityCategoryField")
|
||||
)
|
||||
order = "const PERSONALITY_OPTION_ORDER = " + source.split("const PERSONALITY_OPTION_ORDER = ", 1)[1].split("\n}", 1)[0] + "\n};"
|
||||
result = _run_node(order + functions + """
|
||||
const state = {values: {}, personalityUpdating: {}, personalityMeta: {options: {following:
|
||||
['dom_default', 'close', 'medium', 'far', 'traffic', 'custom', 'legacy_close', 'legacy_medium', 'legacy_far']}}};
|
||||
const PERSONALITY_CATEGORY_DEFINITIONS = {following: {label: 'Following'}};
|
||||
const html = (parts, ...values) => parts.reduce((text, part, i) => {
|
||||
let value = values[i];
|
||||
if (typeof value === 'function') value = part.endsWith('@click="') ? '' : value();
|
||||
return text + part + (Array.isArray(value) ? value.join('') : value ?? '');
|
||||
}, '');
|
||||
console.log(JSON.stringify(['legacy_close', 'legacy_medium', 'legacy_far', 'traffic', 'medium'].map(preset =>
|
||||
renderPersonalityCategoryField({id: 'standard', label: 'Standard'}, 'following', {preset, curve: []}))));
|
||||
""")
|
||||
import re
|
||||
for index, rendered in enumerate(result):
|
||||
buttons = re.findall(r'<button.*?</button>', rendered, flags=re.S)
|
||||
assert sum('aria-pressed="true"' in button for button in buttons) == 1
|
||||
assert any(re.search(r'>\s*Traffic\s*</button>', button) for button in buttons)
|
||||
if index < 3:
|
||||
expected = ["Previous Close", "Previous Medium", "Previous Far"][index]
|
||||
assert any(expected in button and 'aria-pressed="true"' in button for button in buttons)
|
||||
assert 'Previous fixed following distance retained.' in rendered
|
||||
else:
|
||||
assert 'Previous ' not in rendered
|
||||
|
||||
Reference in New Issue
Block a user