mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-15 12:04:00 +08:00
Remember custom personality graphs and correct default resets
This commit is contained in:
committed by
firestar5683
parent
a51205e302
commit
ab6c97fef8
@@ -0,0 +1,44 @@
|
||||
# Custom personality graphs
|
||||
|
||||
Each personality keeps its own Custom acceleration, braking and following curve.
|
||||
Selecting a named preset changes the active selection without deleting Custom
|
||||
points. Selecting Custom again restores those points, including after a reload
|
||||
or restart. If a category has never had Custom points, it is initialized from
|
||||
the current selection, as before.
|
||||
|
||||
The existing **Reset to default** button, below each Custom graph's numeric
|
||||
points in New Galaxy's Advanced section, replaces only that category's Custom
|
||||
curve. It leaves the category set to Custom. The server resolves the reset
|
||||
values; the dashed **Dom default** line uses the same resolver.
|
||||
|
||||
Defaults are Dom's configured base curves sampled at the editor's 10 mph
|
||||
points. They include Traffic's dedicated acceleration and braking, following
|
||||
settings, global tuning switches and powertrain overrides. Where gear mapping
|
||||
is enabled, the reference uses normal gear. Live Eco/Sport gear, weather,
|
||||
lead/stop and overspeed adjustments remain on the existing controller paths.
|
||||
Sampling cannot reproduce every native breakpoint or between-point value;
|
||||
resetting a Custom graph is not the same as delegating to the Dom-default
|
||||
runtime path.
|
||||
|
||||
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.
|
||||
|
||||
## Storage compatibility
|
||||
|
||||
Profile document version 3 retains `curve` and optional `legacyCurve` while
|
||||
`preset` is a named preset or `dom_default`. These retained values are dormant;
|
||||
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.
|
||||
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.
|
||||
|
||||
Older builds do not understand v3 documents. Retain a compatible settings
|
||||
backup before rolling back to one of those builds. Curves discarded before
|
||||
this change cannot be recovered automatically.
|
||||
@@ -8,7 +8,7 @@ import math
|
||||
import numbers
|
||||
|
||||
PERSONALITY_PROFILES_PARAM = "LongitudinalPersonalityProfiles"
|
||||
PROFILE_SCHEMA_VERSION = 2
|
||||
PROFILE_SCHEMA_VERSION = 3
|
||||
PERSONALITY_IDS = ("traffic", "aggressive", "standard", "relaxed")
|
||||
TRUCK_FINGERPRINT_TOKENS = (
|
||||
" RAM 1500 ",
|
||||
@@ -48,6 +48,9 @@ _V2_CURVE_BOUNDS = {
|
||||
"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")
|
||||
@@ -218,6 +221,7 @@ def _validated_category_with_length(
|
||||
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
|
||||
@@ -230,7 +234,7 @@ def _validated_category_with_length(
|
||||
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":
|
||||
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
|
||||
@@ -266,7 +270,7 @@ def _validated_category_with_length(
|
||||
|
||||
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, _V2_CURVE_BOUNDS, _V1_CURVE_BOUNDS)
|
||||
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:
|
||||
@@ -307,6 +311,7 @@ def _strict_document(
|
||||
for category in _CATEGORY_SPECS:
|
||||
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
|
||||
@@ -321,14 +326,23 @@ def _strict_document(
|
||||
|
||||
|
||||
def strict_profile_document(raw_document) -> dict | None:
|
||||
return _strict_document(
|
||||
raw_document,
|
||||
PROFILE_SCHEMA_VERSION,
|
||||
# Version 2 has the same axes and active curves. Read it losslessly; the next
|
||||
# normal save upgrades the document without requiring a destructive reset.
|
||||
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,
|
||||
_V2_CURVE_BOUNDS if version == 2 else _V3_CURVE_BOUNDS,
|
||||
_V1_CURVE_BOUNDS,
|
||||
)
|
||||
if document is not None:
|
||||
document["schemaVersion"] = PROFILE_SCHEMA_VERSION
|
||||
return document
|
||||
|
||||
|
||||
def migrate_profile_document(raw_document) -> dict | None:
|
||||
@@ -406,6 +420,7 @@ def serialize_personality_profiles(profiles, ev_tuning: bool, truck_tuning: bool
|
||||
|
||||
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}")
|
||||
@@ -414,12 +429,14 @@ def update_personality_profile(
|
||||
base_document = profile_document(profiles, enabled=True)
|
||||
canonical = strict_profile_document(base_document)
|
||||
validated = _validated_category(category, {"preset": preset, "curve": curve})
|
||||
if validated is not None and preset == "custom":
|
||||
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 previous["preset"] != "custom" or value != previous["curve"][index]
|
||||
previous is None or not previous["curve"] or value != previous["curve"][index]
|
||||
):
|
||||
validated = None
|
||||
break
|
||||
@@ -436,7 +453,10 @@ def update_personality_profile(
|
||||
base = canonical["profiles"]
|
||||
updated = deepcopy(base)
|
||||
previous = updated[personality][category]
|
||||
if preset == "custom" and previous["preset"] == "custom" and validated["curve"] == previous["curve"]:
|
||||
# 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
|
||||
@@ -522,7 +542,9 @@ def initial_custom_curve(
|
||||
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 preset == "dom_default":
|
||||
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 = [
|
||||
@@ -562,7 +584,7 @@ def interpolate_category_curve(
|
||||
if validated is None:
|
||||
raise ValueError(f"Invalid {category} profile configuration.")
|
||||
values = category_curve(category, validated, ev_tuning, truck_tuning)
|
||||
if "legacyCurve" in validated:
|
||||
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
|
||||
|
||||
@@ -38,7 +38,7 @@ from openpilot.starpilot.common.longitudinal_personality_profiles import (
|
||||
def test_document_is_versioned_disabled_and_declares_exact_axes_and_units():
|
||||
document = profile_document(default_personality_profiles(False), enabled=False)
|
||||
|
||||
assert document["schemaVersion"] == PROFILE_SCHEMA_VERSION == 2
|
||||
assert document["schemaVersion"] == PROFILE_SCHEMA_VERSION == 3
|
||||
assert document["enabled"] is False
|
||||
assert document["axes"] == {
|
||||
"acceleration": {
|
||||
@@ -455,7 +455,7 @@ def test_following_custom_initialisation_uses_effective_legacy_curve():
|
||||
|
||||
|
||||
def test_v2_uses_one_shared_ten_mph_custom_axis():
|
||||
assert PROFILE_SCHEMA_VERSION == 2
|
||||
assert PROFILE_SCHEMA_VERSION == 3
|
||||
expected = tuple(range(0, 91, 10))
|
||||
assert ACCELERATION_SPEEDS_MPH == expected
|
||||
assert BRAKING_SPEEDS_MPH == expected
|
||||
@@ -537,7 +537,7 @@ def test_exact_v1_document_migrates_whole_or_not_at_all():
|
||||
|
||||
migrated = lpp.migrate_profile_document(legacy)
|
||||
assert migrated is not None
|
||||
assert migrated["schemaVersion"] == 2
|
||||
assert migrated["schemaVersion"] == PROFILE_SCHEMA_VERSION
|
||||
assert migrated["enabled"] is True
|
||||
migrated_acceleration = migrated["profiles"]["aggressive"]["acceleration"]
|
||||
assert migrated_acceleration["preset"] == "custom"
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import json
|
||||
from copy import deepcopy
|
||||
|
||||
import pytest
|
||||
|
||||
from openpilot.starpilot.common import longitudinal_personality_profiles as lpp
|
||||
|
||||
|
||||
@pytest.mark.parametrize("personality", lpp.PERSONALITY_IDS)
|
||||
@pytest.mark.parametrize("category,preset", [("acceleration", "eco"), ("braking", "sport"), ("following", "far")])
|
||||
def test_custom_survives_preset_changes_serialization_and_reload(personality, category, preset):
|
||||
profiles = lpp.default_personality_profiles(False)
|
||||
curve = [round(1.0 + i * 0.05, 4) for i in range(10)]
|
||||
profiles = lpp.update_personality_profile(profiles, personality, category, "custom", curve, False)
|
||||
other_profiles = deepcopy(profiles)
|
||||
for selected in (preset, "dom_default", preset):
|
||||
profiles = lpp.update_personality_profile(profiles, personality, category, selected, [], False)
|
||||
profiles = lpp.load_personality_profiles(lpp.serialize_personality_profiles(profiles, False, enabled=True), False)
|
||||
assert profiles[personality][category] == {"preset": selected, "curve": curve}
|
||||
if selected != "dom_default":
|
||||
assert lpp.category_curve(category, profiles[personality][category], False) == lpp.category_curve(category, {"preset": selected, "curve": []}, False)
|
||||
seed = lpp.initial_custom_curve(category, profiles[personality][category], False, False, legacy_curve=[2.0] * 10)
|
||||
profiles = lpp.update_personality_profile(profiles, personality, category, "custom", seed, False)
|
||||
assert profiles == other_profiles
|
||||
|
||||
|
||||
def test_switching_preserves_high_points_and_legacy_runtime_only_for_custom():
|
||||
profiles = lpp.default_personality_profiles(False)
|
||||
saved = {"preset": "custom", "curve": [4.0] * 10, "legacyCurve": [5.0] * 7}
|
||||
profiles["aggressive"]["acceleration"] = deepcopy(saved)
|
||||
profiles = lpp.update_personality_profile(profiles, "aggressive", "acceleration", "eco", [], False)
|
||||
assert profiles["aggressive"]["acceleration"]["legacyCurve"] == saved["legacyCurve"]
|
||||
assert lpp.interpolate_category_curve("acceleration", 12, profiles["aggressive"]["acceleration"], False) == lpp.interpolate_category_curve("acceleration", 12, {"preset": "eco", "curve": []}, False)
|
||||
profiles = lpp.update_personality_profile(profiles, "aggressive", "acceleration", "custom", [4.0] * 10, False)
|
||||
assert profiles["aggressive"]["acceleration"] == saved
|
||||
|
||||
|
||||
def test_v2_load_is_lossless_and_next_write_uses_new_version():
|
||||
document = lpp.profile_document(lpp.default_personality_profiles(False), enabled=True)
|
||||
document["schemaVersion"] = 2
|
||||
document["profiles"]["standard"]["acceleration"] = {"preset": "custom", "curve": [4.0] * 10}
|
||||
loaded = lpp.strict_profile_document(document)
|
||||
assert loaded is not None
|
||||
assert loaded["profiles"] == document["profiles"]
|
||||
assert json.loads(lpp.serialize_personality_profiles(loaded["profiles"], False, enabled=True))["schemaVersion"] > 2
|
||||
document["profiles"]["standard"]["acceleration"]["preset"] = "eco"
|
||||
assert lpp.strict_profile_document(document) is None # v2 never allowed dormant curves
|
||||
|
||||
|
||||
@pytest.mark.parametrize("curve", [[True] * 10, [float("nan")] * 10, [1.0] * 9, [7.0] * 10])
|
||||
def test_dormant_curves_are_validated(curve):
|
||||
profiles = lpp.default_personality_profiles(False)
|
||||
profiles["standard"]["acceleration"] = {"preset": "eco", "curve": curve}
|
||||
assert lpp.strict_profile_document(lpp.profile_document(profiles, enabled=True)) is None
|
||||
@@ -174,14 +174,14 @@ export const PersonalityProfiles = {
|
||||
discard(profile, category) { delete this.drafts[profile + category]; delete this.curveErrors[profile + category] },
|
||||
async saveCurve(profile, category, reset = false) {
|
||||
if (this.editingLocked || this.disposed) return
|
||||
const curve = reset ? this.data.reference_curves?.[profile]?.[category] : this.draft(profile, category)
|
||||
const curve = reset ? [] : this.draft(profile, category)
|
||||
if (!Array.isArray(curve)) return
|
||||
const snapshot = [...curve]
|
||||
this.curvePending = true
|
||||
try {
|
||||
if (this.contextPending) { try { await this.contextRequest } catch { return } }
|
||||
if (this.disposed) return
|
||||
if (await this.write(() => api.savePersonalityProfile({ profile, category, preset: "custom", curve: snapshot, expected: this.data.profiles[profile][category] }), () => !this.locked && !this.data?.migration_required)) this.notice = ""
|
||||
if (await this.write(() => api.savePersonalityProfile({ profile, category, preset: "custom", curve: snapshot, ...(reset ? { reset: true } : {}), expected: this.data.profiles[profile][category] }), () => !this.locked && !this.data?.migration_required)) this.notice = ""
|
||||
} finally {
|
||||
this.discard(profile, category)
|
||||
this.curvePending = false
|
||||
@@ -202,7 +202,7 @@ export const PersonalityProfiles = {
|
||||
if (this.drag?.profile === profile && this.drag.category === category) return this.drag.max
|
||||
return Math.max(this.data.bounds[category][1], ...this.draft(profile, category), ...(this.data.reference_curves?.[profile]?.[category] || []))
|
||||
},
|
||||
graphMin(profile, category) { return this.drag?.profile === profile && this.drag.category === category ? this.drag.min : this.data.bounds[category][0] },
|
||||
graphMin(profile, category) { return this.drag?.profile === profile && this.drag.category === category ? this.drag.min : Math.min(this.data.bounds[category][0], ...this.draft(profile, category), ...(this.data.reference_curves?.[profile]?.[category] || [])) },
|
||||
graphPoints(profile, category, reference = false) {
|
||||
const curve = reference ? this.data.reference_curves?.[profile]?.[category] : this.draft(profile, category)
|
||||
const max = this.graphMax(profile, category)
|
||||
@@ -299,7 +299,7 @@ export const PersonalityProfiles = {
|
||||
<template v-for="(title, category) in CATEGORIES" :key="category">
|
||||
<details v-if="data.profiles[profile][category].preset === 'custom'" open class="gx-personalities__curve">
|
||||
<summary>Custom {{ title.toLowerCase() }} graph</summary>
|
||||
<p>{{ category === 'following' ? 'Seconds' : 'm/s²' }} · {{ speedUnit() }}. Dashed: default.</p>
|
||||
<p>{{ category === 'following' ? 'Seconds' : 'm/s²' }} · {{ speedUnit() }}. Dashed: Dom default.</p>
|
||||
<div class="gx-personalities__plot" tabindex="0" :aria-label="label(profile) + ' ' + title + ' graph; scroll horizontally on narrow screens'">
|
||||
<svg viewBox="-30 -12 340 140" role="group" :aria-disabled="editingLocked" :aria-label="label(profile) + ' ' + title + ' editable curve; exact values below'"
|
||||
style="touch-action: pan-x" @pointerdown="pickPoint(profile, category, $event)" @pointermove="moveDrag" @pointerup="endDrag" @pointercancel="endDrag" @lostpointercapture="endDrag">
|
||||
@@ -336,7 +336,8 @@ export const PersonalityProfiles = {
|
||||
</label>
|
||||
</div>
|
||||
<div class="gx-personalities__options">
|
||||
<button type="button" class="gx-btn gx-btn--tonal" :disabled="editingLocked" @click="saveCurve(profile, category, true)">Reset to default</button>
|
||||
<button type="button" class="gx-btn gx-btn--tonal" :disabled="editingLocked"
|
||||
:aria-label="label(profile) + ' ' + title + ' reset to default'" @click="saveCurve(profile, category, true)">Reset to default</button>
|
||||
</div>
|
||||
</details>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
const assert = require('assert');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = async ({page, data, values, writes, errors, output}) => {
|
||||
const categories = ['acceleration', 'braking', 'following'];
|
||||
for (const category of categories) data.profiles.traffic[category] = {preset:'custom', curve:Array(10).fill(1.15)};
|
||||
data.reference_curves.traffic.braking = Array(10).fill(0.35);
|
||||
const reload = async () => {
|
||||
await page.reload();
|
||||
await page.getByRole('button', {name:'Manage', exact:true}).click();
|
||||
await page.locator('.gx-personalities__profile').first().locator('.gx-personalities__advanced > summary').click();
|
||||
};
|
||||
const settled = () => page.waitForFunction(() => {
|
||||
const vm = document.querySelector('#app').__vue_app__._instance.proxy;
|
||||
return !vm.busy && !vm.curvePending && vm.ready;
|
||||
});
|
||||
await reload();
|
||||
const profile = page.locator('.gx-personalities__profile').first();
|
||||
for (let i=0;i<categories.length;i++) {
|
||||
const category = categories[i];
|
||||
const section = profile.locator('.gx-personalities__category').nth(i);
|
||||
await section.getByRole('button',{name:i===2?'Far':'Eco',exact:true}).click();
|
||||
await settled();
|
||||
await section.getByRole('button',{name:'Custom',exact:true}).click();
|
||||
await settled();
|
||||
assert.deepEqual(data.profiles.traffic[category].curve,Array(10).fill(1.15));
|
||||
const graph = profile.locator('.gx-personalities__curve').nth(i);
|
||||
const reset = graph.getByRole('button',{name:/reset to default/i});
|
||||
const other = JSON.stringify(data.profiles.traffic[categories[(i+1)%3]]);
|
||||
await reset.click();
|
||||
await settled();
|
||||
assert.equal(writes.at(-1).reset,true);
|
||||
assert.deepEqual(writes.at(-1).curve,[]);
|
||||
assert(writes.at(-1).expected);
|
||||
assert.deepEqual(data.profiles.traffic[category].curve,data.reference_curves.traffic[category]);
|
||||
assert.equal(JSON.stringify(data.profiles.traffic[categories[(i+1)%3]]),other);
|
||||
assert.deepEqual(await graph.locator('input').evaluateAll(inputs=>inputs.map(input=>Number(input.value))),data.reference_curves.traffic[category]);
|
||||
assert.equal(await graph.locator('polyline').nth(0).getAttribute('points'),await graph.locator('polyline').nth(1).getAttribute('points'));
|
||||
}
|
||||
await reload();
|
||||
assert.equal(Number(await profile.locator('.gx-personalities__curve').nth(1).locator('input').first().inputValue()),0.35);
|
||||
const points = await profile.locator('.gx-personalities__curve').nth(1).locator('polyline').first().getAttribute('points');
|
||||
assert(points.split(' ').every(pair=>Number(pair.split(',')[1])<=90),'low default stays inside graph axes');
|
||||
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,`custom-reset-${width}.png`),fullPage:true});
|
||||
}
|
||||
values.IsOnroad=true; values.IsOffroad=false;
|
||||
await page.reload();
|
||||
await page.getByRole('button',{name:'Manage',exact:true}).click();
|
||||
await profile.locator('.gx-personalities__advanced > summary').click();
|
||||
for (const button of await profile.getByRole('button',{name:/reset to default/i}).all()) assert(await button.isDisabled());
|
||||
assert.deepEqual(errors,[]);
|
||||
console.log('PASS: rendered reset controls, retained presets, reload persistence, per-category defaults/reference parity, narrow/wide layout and onroad lock');
|
||||
};
|
||||
@@ -17,10 +17,13 @@ const output=process.env.PERSONALITY_BROWSER_OUTPUT || path.join(require('os').t
|
||||
const u=new URL(route.request().url()); let file;
|
||||
if(u.pathname==='/')return route.fulfill({contentType:'text/html',body:`<script type="importmap">{"imports":{"vue":"/assets/vendor/vue/vue.esm-browser.js"}}</script><link rel="stylesheet" href="/assets/vendor/bootstrap-icons/bootstrap-icons.min.css"><link rel="stylesheet" href="/assets/mobile/css/material.css"><div id="app"></div><div id="snackbar_wrapper"></div><script type="module">import {createApp} from '/assets/vendor/vue/vue.esm-browser.js';import {PersonalityProfiles} from '/assets/mobile/js/components/PersonalityProfiles.js';createApp(PersonalityProfiles).mount('#app');</script>`});
|
||||
if(u.pathname==='/api/params/all'){await waitGate('params');return route.fulfill({json:values});}
|
||||
if(u.pathname==='/api/longitudinal_mode' && route.request().method()==='GET')return route.fulfill({json:{mode:'chill',locked:false,reason:'',experimental_confirmed:false,values:{ExperimentalMode:false,ConditionalExperimental:false,ConditionalChill:false}}});
|
||||
if(u.pathname==='/api/params/defaults')return route.fulfill({json:{}});
|
||||
if(u.pathname==='/api/params'){const d=route.request().postDataJSON();values[d.key]=d.value;return route.fulfill({json:{success:true}});}
|
||||
if(u.pathname==='/api/personality_profiles'){
|
||||
if(route.request().method()==='PUT'){attempts++;await waitGate('put');if(failWrite||faults.failPut){failWrite=false;faults.failPut=false;return route.fulfill({status:503,json:{error:'Synthetic save failure'}});}const d=route.request().postDataJSON();writes.push(d);data.profiles[d.profile][d.category]={preset:d.preset,curve:d.curve.length?d.curve:[...data.reference_curves[d.profile][d.category]]};}
|
||||
if(route.request().method()==='PUT'){attempts++;await waitGate('put');if(failWrite||faults.failPut){failWrite=false;faults.failPut=false;return route.fulfill({status:503,json:{error:'Synthetic save failure'}});}const d=route.request().postDataJSON();writes.push(d);const previous=data.profiles[d.profile][d.category];
|
||||
if(d.expected && JSON.stringify(d.expected)!==JSON.stringify(previous))return route.fulfill({status:409,json:{error:'Saved profile changed'}});
|
||||
data.profiles[d.profile][d.category]={preset:d.preset,curve:d.reset?[...data.reference_curves[d.profile][d.category]]:d.preset==='custom'?(d.curve.length?d.curve:previous.curve.length?[...previous.curve]:[...data.reference_curves[d.profile][d.category]]):[...previous.curve]};}
|
||||
else {profileReads++;if(faults.readFailures){faults.readFailures--;return route.fulfill({status:503,json:{error:'Synthetic readback failure'}});}}
|
||||
return route.fulfill({json:data});}
|
||||
if(u.pathname.endsWith('device_settings_layout.json'))file=root+'/starpilot/common/assets/device_settings_layout.json';
|
||||
@@ -37,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_CUSTOM_ONLY){
|
||||
await require('./personality_custom_graphs.cjs')({page,data,values,writes,errors,output});
|
||||
return;
|
||||
}
|
||||
if(process.env.PERSONALITY_POLL_ONLY){
|
||||
await require('./personality_poll.cjs')({page,data,values,faults,counts:()=>({attempts}),errors});
|
||||
return;
|
||||
@@ -130,7 +137,7 @@ const output=process.env.PERSONALITY_BROWSER_OUTPUT || path.join(require('os').t
|
||||
await n.press('Tab');
|
||||
await page.waitForFunction(()=>!document.querySelector('#app').__vue_app__._instance.proxy.curvePending);
|
||||
assert.equal(Number(await n.inputValue()),1.5);
|
||||
await graph.getByRole('button',{name:'Reset to default',exact:true}).click();
|
||||
await graph.getByRole('button',{name:/reset to default/i}).click();
|
||||
await page.waitForFunction(()=>!document.querySelector('#app').__vue_app__._instance.proxy.busy);
|
||||
const p=['traffic','aggressive','standard','relaxed'][pi],c=['acceleration','braking','following'][ci];
|
||||
assert.deepEqual(await graph.locator('input').evaluateAll(ns=>ns.map(n=>Number(n.value))),data.reference_curves[p][c]);
|
||||
@@ -204,7 +211,7 @@ const output=process.env.PERSONALITY_BROWSER_OUTPUT || path.join(require('os').t
|
||||
store.route='/settings/longitudinal-speed-following';store.params={open:'CustomPersonalities'};store.search='';createApp(Settings).mount('#app');
|
||||
});
|
||||
await page.locator('.gx-personalities__grid').waitFor();assert(await page.locator('#gx-personality-settings').isVisible());
|
||||
assert.equal(await page.locator('.gx-longitudinal-mode').count(),0,'personality-only settings do not introduce unified mode');
|
||||
assert.equal(await page.locator('.gx-longitudinal-mode').count(),1,'Dom Settings retains the existing unified mode selector');
|
||||
for(const theme of ['dark','light']) {
|
||||
await page.evaluate(t=>document.documentElement.dataset.theme=t,theme);
|
||||
await page.locator('.gx-personalities__heading').scrollIntoViewIfNeeded();
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
from copy import deepcopy
|
||||
|
||||
import pytest
|
||||
|
||||
from openpilot.starpilot.common import longitudinal_personality_profiles as lpp
|
||||
from openpilot.starpilot.common.accel_profile import A_CRUISE_MAX_VALS_TRAFFIC_ALL, interpolate_accel_profile, get_accel_profile_curve_values
|
||||
from test_personality_profiles_api import _client, the_galaxy
|
||||
|
||||
|
||||
@pytest.mark.parametrize("personality", lpp.PERSONALITY_IDS)
|
||||
@pytest.mark.parametrize("category,preset", [("acceleration", "eco"), ("braking", "sport"), ("following", "far")])
|
||||
def test_retention_and_reset_are_atomic_per_category(monkeypatch, personality, category, preset):
|
||||
profiles = lpp.default_personality_profiles(False)
|
||||
saved = {"preset": "custom", "curve": [1.15] * 10}
|
||||
profiles[personality][category] = deepcopy(saved)
|
||||
client, params = _client(monkeypatch, {"CustomPersonalities": True, lpp.PERSONALITY_PROFILES_PARAM: lpp.profile_document(profiles, enabled=True)})
|
||||
endpoint = "/api/personality_profiles"
|
||||
for selected in (preset, "dom_default", "custom"):
|
||||
expected = client.get(endpoint).get_json()["profiles"][personality][category]
|
||||
response = client.put(endpoint, json={"profile": personality, "category": category, "preset": selected, "curve": [], "expected": expected})
|
||||
assert response.status_code == 200
|
||||
assert response.get_json()["profiles"][personality][category]["curve"] == saved["curve"]
|
||||
before = client.get(endpoint).get_json()
|
||||
response = client.put(endpoint, json={"profile": personality, "category": category, "preset": "custom", "curve": [], "reset": True, "expected": saved})
|
||||
assert response.status_code == 200
|
||||
result = response.get_json()
|
||||
expected_profiles = deepcopy(profiles)
|
||||
expected_profiles[personality][category] = {"preset": "custom", "curve": before["reference_curves"][personality][category]}
|
||||
assert result["profiles"] == expected_profiles
|
||||
assert lpp.strict_profile_document(params.values[lpp.PERSONALITY_PROFILES_PARAM])["profiles"] == expected_profiles
|
||||
for selected in (preset, "custom"):
|
||||
response = client.put(endpoint, json={"profile": personality, "category": category, "preset": selected, "curve": []})
|
||||
assert response.status_code == 200
|
||||
assert response.get_json()["profiles"] == expected_profiles
|
||||
|
||||
|
||||
def test_reference_lines_use_dom_traffic_and_enabled_following_defaults(monkeypatch):
|
||||
client, _ = _client(monkeypatch, {"CustomPersonalities": True})
|
||||
curves = client.get("/api/personality_profiles").get_json()["reference_curves"]
|
||||
assert curves["traffic"]["acceleration"] == [round(interpolate_accel_profile(speed * 0.44704, A_CRUISE_MAX_VALS_TRAFFIC_ALL), 4) for speed in lpp.ACCELERATION_SPEEDS_MPH]
|
||||
assert curves["traffic"]["braking"] == [0.35] * 10
|
||||
assert curves["standard"]["braking"] == [0.5] * 10
|
||||
assert curves["standard"]["following"] == [1.45] * 5 + [1.4, 1.3, 1.2, 1.2, 1.2]
|
||||
assert curves["traffic"]["following"][0] == 0.75
|
||||
assert curves["traffic"]["following"][-1] == 1.6
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tuning", [False, True])
|
||||
def test_reference_respects_global_tuning_switches_and_powertrain_overrides(monkeypatch, tuning):
|
||||
client, _ = _client(monkeypatch, {"CustomPersonalities": True, "LongitudinalTune": tuning, "AccelerationProfile": 2, "DecelerationProfile": 2, "EVTuning": False, "TruckTuning": True}, ev_tuning=True)
|
||||
curves = client.get("/api/personality_profiles").get_json()["reference_curves"]
|
||||
expected = get_accel_profile_curve_values(2 if tuning else 0, False, True)
|
||||
assert curves["standard"]["acceleration"] == [round(interpolate_accel_profile(speed * 0.44704, expected), 4) for speed in lpp.ACCELERATION_SPEEDS_MPH]
|
||||
assert curves["standard"]["braking"] == [2.0 if tuning else 0.5] * 10
|
||||
|
||||
|
||||
@pytest.mark.parametrize("extra", [{"reset": "true"}, {"reset": 1}, {"reset": True, "preset": "eco"}, {"reset": True, "curve": [1.0] * 10}])
|
||||
def test_malformed_reset_never_writes(monkeypatch, extra):
|
||||
client, params = _client(monkeypatch)
|
||||
response = client.put("/api/personality_profiles", json={"profile": "standard", "category": "acceleration", "preset": "custom", "curve": [], **extra})
|
||||
assert response.status_code == 400
|
||||
assert params.writes == []
|
||||
|
||||
|
||||
def test_reset_obeys_offroad_and_stale_editor_guards(monkeypatch):
|
||||
client, params = _client(monkeypatch, {"IsOnroad": True})
|
||||
payload = {"profile": "standard", "category": "following", "preset": "custom", "curve": [], "reset": True}
|
||||
assert client.put("/api/personality_profiles", json=payload).status_code == 403
|
||||
assert params.writes == []
|
||||
params.values.update(IsOnroad=False, IsOffroad=True)
|
||||
payload["expected"] = {"preset": "custom", "curve": [1.0] * 10}
|
||||
assert client.put("/api/personality_profiles", json=payload).status_code == 409
|
||||
assert params.writes == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("personality,category,values,expected", [
|
||||
("traffic", "braking", {}, 0.35),
|
||||
("standard", "acceleration", {"TruckTuning": True}, 6.0),
|
||||
])
|
||||
def test_first_custom_default_and_reset_keep_dom_points_outside_authoring_bounds(monkeypatch, personality, category, values, expected):
|
||||
client, params = _client(monkeypatch, {"CustomPersonalities": True, **values})
|
||||
endpoint = "/api/personality_profiles"
|
||||
payload = {"profile": personality, "category": category, "preset": "custom", "curve": []}
|
||||
response = client.put(endpoint, json=payload)
|
||||
assert response.status_code == 200
|
||||
config = response.get_json()["profiles"][personality][category]
|
||||
assert config["curve"][0] == expected
|
||||
original = deepcopy(config["curve"])
|
||||
# Editing one point retains the default-only values at the other points.
|
||||
edited = list(original)
|
||||
edited[-1] = 1.0
|
||||
response = client.put(endpoint, json={**payload, "curve": edited})
|
||||
assert response.status_code == 200
|
||||
assert response.get_json()["profiles"][personality][category]["curve"] == edited
|
||||
response = client.put(endpoint, json={**payload, "reset": True})
|
||||
assert response.status_code == 200
|
||||
assert response.get_json()["profiles"][personality][category]["curve"] == original
|
||||
invalid = list(original)
|
||||
invalid[0] = 0.4 if category == "braking" else 5.5
|
||||
before = deepcopy(params.values)
|
||||
assert client.put(endpoint, json={**payload, "curve": invalid}).status_code == 400
|
||||
assert params.values == before
|
||||
|
||||
|
||||
def test_reset_retires_legacy_interpolation_even_if_display_points_match(monkeypatch):
|
||||
profiles = lpp.default_personality_profiles(False)
|
||||
profiles["standard"]["braking"] = {"preset": "custom", "curve": [0.5] * 10, "legacyCurve": [1.0] * 7}
|
||||
client, _ = _client(monkeypatch, {lpp.PERSONALITY_PROFILES_PARAM: lpp.profile_document(profiles, enabled=True)})
|
||||
result = client.put("/api/personality_profiles", json={"profile": "standard", "category": "braking", "preset": "custom", "curve": [], "reset": True})
|
||||
assert result.status_code == 200
|
||||
assert result.get_json()["profiles"]["standard"]["braking"] == {"preset": "custom", "curve": [0.5] * 10}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("enabled,low,high", [(True, 0.5, 1.1), (False, 0.75, 1.6)])
|
||||
def test_traffic_following_defaults_honor_master_and_legacy_minimum(monkeypatch, enabled, low, high):
|
||||
client, _ = _client(monkeypatch, {"CustomPersonalities": enabled, "TrafficFollow": 0.5, "RelaxedFollow": 1.1})
|
||||
payload = {"profile": "traffic", "category": "following", "preset": "custom", "curve": [], "reset": True}
|
||||
result = client.put("/api/personality_profiles", json=payload)
|
||||
assert result.status_code == 200
|
||||
body = result.get_json()
|
||||
curve = body["profiles"]["traffic"]["following"]["curve"]
|
||||
assert curve == body["reference_curves"]["traffic"]["following"]
|
||||
assert (curve[0], curve[-1]) == (low, high)
|
||||
|
||||
|
||||
def test_default_reference_uses_normal_gear_when_mapping_is_enabled(monkeypatch):
|
||||
client, _ = _client(monkeypatch, {"QOLLongitudinal": True, "MapGears": True, "MapAcceleration": True, "MapDeceleration": True, "LongitudinalTune": True, "AccelerationProfile": 2, "DecelerationProfile": 2})
|
||||
curves = client.get("/api/personality_profiles").get_json()["reference_curves"]
|
||||
expected = get_accel_profile_curve_values(0, False, False)
|
||||
assert curves["standard"]["acceleration"] == [round(interpolate_accel_profile(speed * 0.44704, expected), 4) for speed in lpp.ACCELERATION_SPEEDS_MPH]
|
||||
assert curves["standard"]["braking"] == [1.0] * 10
|
||||
@@ -1,8 +1,10 @@
|
||||
import json
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from openpilot.starpilot.common import accel_profile
|
||||
from openpilot.starpilot.common.accel_profile import A_CRUISE_MAX_BP_CUSTOM, ACCELERATION_PROFILES, interpolate_accel_profile
|
||||
from openpilot.starpilot.common.longitudinal_personality_profiles import (
|
||||
FOLLOWING_SPEEDS_MPH,
|
||||
@@ -22,6 +24,11 @@ from test_navigation_params import _params_client, the_galaxy
|
||||
|
||||
|
||||
def _client(monkeypatch, values=None, *, ev_tuning=False, truck_tuning=False):
|
||||
# Use the real pure curve implementation; the general Galaxy import fixture
|
||||
# stubs it with incomplete vehicle tables for unrelated dashboard tests.
|
||||
monkeypatch.setitem(sys.modules, "openpilot.starpilot.common.accel_profile", accel_profile)
|
||||
for name in ("get_accel_profile_curve_values", "interpolate_accel_profile", "normalize_acceleration_profile", "normalize_deceleration_profile"):
|
||||
monkeypatch.setattr(the_galaxy, name, getattr(accel_profile, name))
|
||||
device_values = dict(values or {})
|
||||
device_values.setdefault("IsOnroad", False)
|
||||
device_values.setdefault("IsOffroad", not device_values["IsOnroad"])
|
||||
@@ -248,7 +255,7 @@ def test_legacy_master_without_document_remains_enabled_when_first_profile_is_sa
|
||||
assert document is not None and document["enabled"] is True
|
||||
|
||||
|
||||
def test_first_save_persists_one_atomic_versioned_document_with_other_categories_standard(monkeypatch):
|
||||
def test_first_save_persists_one_atomic_versioned_document_with_other_categories_dom_default(monkeypatch):
|
||||
client, params = _client(monkeypatch)
|
||||
response = client.put("/api/personality_profiles", json={
|
||||
"profile": "standard", "category": "braking", "preset": "sport", "curve": [2.0] * 10,
|
||||
@@ -262,7 +269,7 @@ def test_first_save_persists_one_atomic_versioned_document_with_other_categories
|
||||
for category, config in profile.items():
|
||||
if (profile_id, category) != ("standard", "braking"):
|
||||
assert config == {
|
||||
"preset": "medium" if category == "following" else "standard", "curve": [],
|
||||
"preset": "dom_default", "curve": [],
|
||||
}
|
||||
assert len([write for write in params.writes if write[0] == PERSONALITY_PROFILES_PARAM]) == 1
|
||||
|
||||
@@ -345,7 +352,7 @@ def test_dom_default_custom_acceleration_seeds_from_effective_legacy_custom_curv
|
||||
profiles["traffic"]["acceleration"] = {"preset": "dom_default", "curve": []}
|
||||
values = {
|
||||
"IsOnroad": False,
|
||||
"CustomAccelProfile": True,
|
||||
"CustomAccelProfile": True, "AdvancedLongitudinalTune": True,
|
||||
"CustomAccelProfileInitialized": True,
|
||||
PERSONALITY_PROFILES_PARAM: profile_document(profiles, enabled=False),
|
||||
**{
|
||||
@@ -360,7 +367,7 @@ def test_dom_default_custom_acceleration_seeds_from_effective_legacy_custom_curv
|
||||
assert response.status_code == 200
|
||||
document = strict_profile_document(params.values[PERSONALITY_PROFILES_PARAM])
|
||||
expected = [
|
||||
round(interpolate_accel_profile(speed * 0.44704, [1.1, 1.0, 0.9, 0.8, 0.7, 0.6, 0.5], A_CRUISE_MAX_BP_CUSTOM), 4)
|
||||
round(interpolate_accel_profile(speed * 0.44704, accel_profile.A_CRUISE_MAX_VALS_TRAFFIC_ALL, A_CRUISE_MAX_BP_CUSTOM), 4)
|
||||
for speed in FOLLOWING_SPEEDS_MPH
|
||||
]
|
||||
assert document["profiles"]["traffic"]["acceleration"]["curve"] == expected
|
||||
@@ -386,7 +393,7 @@ def test_dom_default_custom_seed_resamples_valid_dynamic_curve_and_malformed_dyn
|
||||
profiles["standard"]["acceleration"] = {"preset": "dom_default", "curve": []}
|
||||
dynamic = {
|
||||
"IsOnroad": False,
|
||||
"CustomAccelProfile": True,
|
||||
"CustomAccelProfile": True, "AdvancedLongitudinalTune": True,
|
||||
PERSONALITY_PROFILES_PARAM: profile_document(profiles, enabled=False),
|
||||
"CustomAccelProfileBreakpointsInitialized": True,
|
||||
"CustomAccelProfilePointCount": 3,
|
||||
@@ -450,7 +457,7 @@ def test_following_custom_seeds_from_legacy_profile_and_then_persists_edits(monk
|
||||
assert document["profiles"]["standard"]["following"]["curve"] == [round(value, 4) for value in edited]
|
||||
|
||||
|
||||
def test_fresh_following_custom_seeds_from_selected_medium_even_when_legacy_custom_is_off(monkeypatch):
|
||||
def test_fresh_following_custom_seeds_from_dom_default_when_legacy_custom_is_off(monkeypatch):
|
||||
client, params = _client(monkeypatch, {
|
||||
"IsOnroad": False,
|
||||
"CustomPersonalities": False,
|
||||
@@ -462,15 +469,15 @@ def test_fresh_following_custom_seeds_from_selected_medium_even_when_legacy_cust
|
||||
})
|
||||
assert response.status_code == 200
|
||||
document = strict_profile_document(params.values[PERSONALITY_PROFILES_PARAM])
|
||||
assert document["profiles"]["relaxed"]["following"]["curve"] == [1.45] * len(FOLLOWING_SPEEDS_MPH)
|
||||
assert document["profiles"]["relaxed"]["following"]["curve"] == [1.75] * len(FOLLOWING_SPEEDS_MPH)
|
||||
|
||||
|
||||
def test_traffic_following_seed_matches_legacy_runtime_speed_units(monkeypatch):
|
||||
profiles = default_personality_profiles(False)
|
||||
profiles["traffic"]["following"] = {"preset": "dom_default", "curve": []}
|
||||
client, params = _client(monkeypatch, {
|
||||
"IsOnroad": False,
|
||||
PERSONALITY_PROFILES_PARAM: profile_document(profiles, enabled=False),
|
||||
"IsOnroad": False, "CustomPersonalities": True,
|
||||
PERSONALITY_PROFILES_PARAM: profile_document(profiles, enabled=True),
|
||||
"TrafficFollow": 0.8,
|
||||
"RelaxedFollow": 1.6,
|
||||
})
|
||||
@@ -828,7 +835,7 @@ def test_known_v1_document_is_migrated_for_readback(monkeypatch):
|
||||
|
||||
assert body["configured"] is True
|
||||
assert body["migration_required"] is True
|
||||
assert body["schema_version"] == 2
|
||||
assert body["schema_version"] == PROFILE_SCHEMA_VERSION
|
||||
assert len(body["profiles"]["standard"]["acceleration"]["curve"]) == 10
|
||||
assert body["profiles"]["standard"]["acceleration"]["legacyCurve"] == [1.0] * 7
|
||||
|
||||
|
||||
@@ -519,7 +519,7 @@ def test_personality_cards_and_advanced_disclosures_have_unique_accessible_relat
|
||||
assert 'aria-controls="personality-advanced-${profile.id}"' in advanced
|
||||
assert 'id="personality-advanced-${profile.id}"' in source
|
||||
assert 'aria-hidden="true"' in advanced
|
||||
manage = source.split('${() => p.is_parent_toggle', 1)[1].split("` : \"\"}", 1)[0]
|
||||
manage = source.split('p.is_parent_toggle && (p.key === "CustomPersonalities"', 1)[1].split("` : \"\"}", 1)[0]
|
||||
assert 'aria-controls="${p.key === "CustomPersonalities" ? "personality-profiles-panel"' in manage
|
||||
assert 'aria-expanded="${() => state.expanded[p.key] ? "true" : "false"}"' in manage
|
||||
|
||||
|
||||
@@ -150,7 +150,6 @@ from openpilot.starpilot.common.longitudinal_personality_profiles import (
|
||||
initial_custom_curve,
|
||||
is_truck_fingerprint,
|
||||
migrate_profile_document,
|
||||
personality_reference_curves,
|
||||
profile_document,
|
||||
strict_profile_document,
|
||||
synchronise_profile_document_enabled,
|
||||
@@ -3722,18 +3721,20 @@ def _get_detected_truck_tuning():
|
||||
return False
|
||||
|
||||
|
||||
def _get_effective_legacy_custom_accel_curve(ev_tuning: bool, truck_tuning: bool) -> list[float]:
|
||||
def _get_effective_legacy_custom_accel_curve(
|
||||
ev_tuning: bool, truck_tuning: bool, *, acceleration_profile=None, custom_enabled: bool | None = None,
|
||||
) -> list[float]:
|
||||
target_axis = np.array(ACCELERATION_SPEEDS_MPH, dtype=float) * 0.44704
|
||||
|
||||
def sample(values, breakpoints):
|
||||
return [round(interpolate_accel_profile(float(speed), values, breakpoints), 4) for speed in target_axis]
|
||||
|
||||
preset_curve = get_accel_profile_curve_values(
|
||||
normalize_acceleration_profile(_safe_params_get_live_raw("AccelerationProfile")),
|
||||
normalize_acceleration_profile(_safe_params_get_live_raw("AccelerationProfile") if acceleration_profile is None else acceleration_profile),
|
||||
ev_tuning,
|
||||
truck_tuning,
|
||||
)
|
||||
if not _safe_params_get_bool("CustomAccelProfile"):
|
||||
if not (_safe_params_get_bool("CustomAccelProfile") if custom_enabled is None else custom_enabled):
|
||||
return sample(preset_curve, A_CRUISE_MAX_BP_CUSTOM)
|
||||
|
||||
raw_legacy = {key: _safe_params_get_live_raw(key) for key in CUSTOM_ACCEL_PROFILE_PARAM_KEYS}
|
||||
@@ -3779,12 +3780,12 @@ def _get_effective_legacy_following_curve(profile_id: str) -> list[float]:
|
||||
|
||||
def follow_value(key: str) -> float:
|
||||
try:
|
||||
parsed = float(_safe_params_get_live_raw(key, defaults[key]))
|
||||
parsed = float(_safe_params_get_live_raw(key, defaults[key]) if _safe_params_get_bool("CustomPersonalities") else defaults[key])
|
||||
except (TypeError, ValueError):
|
||||
parsed = defaults[key]
|
||||
if not math.isfinite(parsed):
|
||||
parsed = defaults[key]
|
||||
return float(np.clip(parsed, *CURVE_BOUNDS["following"]))
|
||||
return float(np.clip(parsed, 0.5 if key == "TrafficFollow" else CURVE_BOUNDS["following"][0], CURVE_BOUNDS["following"][1]))
|
||||
|
||||
if profile_id == "traffic":
|
||||
breakpoints = (0.0, 25.0 / CV.MPH_TO_MS)
|
||||
@@ -3798,6 +3799,43 @@ def _get_effective_legacy_following_curve(profile_id: str) -> list[float]:
|
||||
return [round(float(point), 4) for point in np.interp(FOLLOWING_SPEEDS_MPH, breakpoints, values)]
|
||||
|
||||
|
||||
def _get_dom_personality_reference_curves(ev_tuning: bool) -> dict[str, dict[str, list[float]]]:
|
||||
"""Sample Dom's configured base curves, without live driving modifiers.
|
||||
|
||||
Use global powertrain/tuning switches for Dom default, just as the runtime
|
||||
does. Named personality presets have a separate detected-powertrain policy.
|
||||
"""
|
||||
from openpilot.starpilot.common.accel_profile import A_CRUISE_MAX_VALS_TRAFFIC_ALL
|
||||
|
||||
truck_tuning = _safe_params_get_bool("TruckTuning")
|
||||
raw_ev = _safe_params_get_live_raw("EVTuning")
|
||||
ev_tuning = (ev_tuning if raw_ev in (None, b"", "") else _safe_params_get_bool("EVTuning")) and not truck_tuning
|
||||
tuning = _safe_params_get_bool("LongitudinalTune")
|
||||
custom_accel = _safe_params_get_bool("AdvancedLongitudinalTune") and _safe_params_get_bool("CustomAccelProfile")
|
||||
acceleration_profile = normalize_acceleration_profile(_safe_params_get_live_raw("AccelerationProfile")) if tuning or custom_accel else 0
|
||||
deceleration_profile = normalize_deceleration_profile(_safe_params_get_live_raw("DecelerationProfile", 1)) if tuning else 1
|
||||
map_gears = _safe_params_get_bool("QOLLongitudinal") and _safe_params_get_bool("MapGears")
|
||||
# A static speed graph uses the normal-gear base; live Eco/Sport, weather and
|
||||
# overspeed/lead modifiers remain on Dom's existing controller paths.
|
||||
if map_gears and _safe_params_get_bool("MapAcceleration") and not custom_accel:
|
||||
acceleration_profile = 0
|
||||
if map_gears and _safe_params_get_bool("MapDeceleration"):
|
||||
deceleration_profile = 0
|
||||
acceleration = _get_effective_legacy_custom_accel_curve(
|
||||
ev_tuning, truck_tuning, acceleration_profile=acceleration_profile, custom_enabled=custom_accel,
|
||||
)
|
||||
traffic_acceleration = [round(interpolate_accel_profile(speed * CV.MPH_TO_MS, A_CRUISE_MAX_VALS_TRAFFIC_ALL), 4)
|
||||
for speed in ACCELERATION_SPEEDS_MPH]
|
||||
return {
|
||||
profile: {
|
||||
"acceleration": list(traffic_acceleration if profile == "traffic" else acceleration),
|
||||
"braking": [0.35 if profile == "traffic" else {0: 1.0, 1: 0.5, 2: 2.0}[deceleration_profile]] * len(BRAKING_SPEEDS_MPH),
|
||||
"following": _get_effective_legacy_following_curve(profile),
|
||||
}
|
||||
for profile in ("traffic", "aggressive", "standard", "relaxed")
|
||||
}
|
||||
|
||||
|
||||
def _get_runtime_default_param_overrides():
|
||||
overrides = {}
|
||||
static_defaults = _get_static_default_param_values()
|
||||
@@ -6003,8 +6041,11 @@ def setup(app):
|
||||
return jsonify({"error": "Stored longitudinal personality profiles require a verified migration before editing."}), 409
|
||||
data = request.get_json(silent=True)
|
||||
required_fields = {"profile", "category", "preset", "curve"}
|
||||
if not isinstance(data, dict) or set(data) not in (required_fields, required_fields | {"expected"}):
|
||||
return jsonify({"error": "Expected profile, category, preset, curve, and optional expected category."}), 400
|
||||
if not isinstance(data, dict) or not required_fields <= set(data) or set(data) - required_fields - {"expected", "reset"}:
|
||||
return jsonify({"error": "Expected profile, category, preset, curve, and optional expected category or reset."}), 400
|
||||
reset = data.get("reset", False)
|
||||
if type(reset) is not bool or (reset and (data["preset"] != "custom" or data["curve"] != [])):
|
||||
return jsonify({"error": "Reset requires Custom and an empty curve; defaults are resolved by the server."}), 400
|
||||
|
||||
try:
|
||||
current_config = profiles[data["profile"]][data["category"]]
|
||||
@@ -6013,23 +6054,17 @@ def setup(app):
|
||||
)):
|
||||
return jsonify({"error": "Saved profile changed. Reload and review it before editing again."}), 409
|
||||
curve = data["curve"]
|
||||
if data["preset"] == "custom" and current_config.get("preset") != "custom":
|
||||
initialize_default = data["preset"] == "custom" and current_config["preset"] == "dom_default" and not current_config["curve"]
|
||||
if reset:
|
||||
curve = _get_dom_personality_reference_curves(ev_tuning)[data["profile"]][data["category"]]
|
||||
elif data["preset"] == "custom" and current_config.get("preset") != "custom":
|
||||
if curve != []:
|
||||
update_personality_profile(
|
||||
profiles, data["profile"], data["category"], "custom", curve, ev_tuning, truck_tuning
|
||||
)
|
||||
legacy_curve = None
|
||||
if current_config.get("preset") == "dom_default":
|
||||
if data["category"] == "acceleration":
|
||||
legacy_curve = _get_effective_legacy_custom_accel_curve(ev_tuning, truck_tuning)
|
||||
elif data["category"] == "braking":
|
||||
legacy_curve = {
|
||||
0: [1.0] * len(BRAKING_SPEEDS_MPH),
|
||||
1: [0.5] * len(BRAKING_SPEEDS_MPH),
|
||||
2: [2.0] * len(BRAKING_SPEEDS_MPH),
|
||||
}[normalize_deceleration_profile(_safe_params_get_live_raw("DecelerationProfile"))]
|
||||
else:
|
||||
legacy_curve = _get_effective_legacy_following_curve(data["profile"])
|
||||
if current_config.get("preset") == "dom_default" and not current_config.get("curve"):
|
||||
legacy_curve = _get_dom_personality_reference_curves(ev_tuning)[data["profile"]][data["category"]]
|
||||
curve = initial_custom_curve(
|
||||
data["category"], current_config, ev_tuning, truck_tuning, legacy_curve=legacy_curve
|
||||
)
|
||||
@@ -6043,6 +6078,7 @@ def setup(app):
|
||||
curve,
|
||||
ev_tuning,
|
||||
truck_tuning,
|
||||
reset=reset or initialize_default,
|
||||
)
|
||||
except (KeyError, TypeError, ValueError) as error:
|
||||
return jsonify({"error": str(error)}), 400
|
||||
@@ -6064,7 +6100,7 @@ def setup(app):
|
||||
"following": list(FOLLOWING_PRESETS),
|
||||
},
|
||||
"profiles": profiles,
|
||||
"reference_curves": personality_reference_curves(ev_tuning, truck_tuning),
|
||||
"reference_curves": _get_dom_personality_reference_curves(ev_tuning),
|
||||
"schema_version": PROFILE_SCHEMA_VERSION,
|
||||
"speed_breakpoints_mph": {
|
||||
"acceleration": list(ACCELERATION_SPEEDS_MPH),
|
||||
|
||||
Reference in New Issue
Block a user