mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-18 05:23:57 +08:00
fix
This commit is contained in:
@@ -1203,7 +1203,6 @@
|
||||
},
|
||||
{
|
||||
"key": "CustomPersonalities",
|
||||
"requires_offroad": true,
|
||||
"label": "Driving Personalities",
|
||||
"description": "Customize braking, acceleration, and following distance for each profile.",
|
||||
"picker_description": "Customizes driving personalities to match your style.",
|
||||
@@ -1214,7 +1213,6 @@
|
||||
},
|
||||
{
|
||||
"key": "TrafficPersonalityProfile",
|
||||
"requires_offroad": true,
|
||||
"label": "Traffic Mode",
|
||||
"description": "Customize the \"Traffic Mode\" personality profile. Designed for stop-and-go driving.",
|
||||
"picker_description": "Customizes Traffic Mode for stop-and-go driving.",
|
||||
@@ -1226,7 +1224,6 @@
|
||||
},
|
||||
{
|
||||
"key": "AggressivePersonalityProfile",
|
||||
"requires_offroad": true,
|
||||
"label": "Aggressive",
|
||||
"description": "Customize the \"Aggressive\" personality profile. Designed for assertive driving with tighter gaps.",
|
||||
"picker_description": "Customizes Aggressive Mode for assertive driving.",
|
||||
@@ -1238,7 +1235,6 @@
|
||||
},
|
||||
{
|
||||
"key": "StandardPersonalityProfile",
|
||||
"requires_offroad": true,
|
||||
"label": "Standard",
|
||||
"description": "Customize the \"Standard\" personality profile. Designed for balanced driving with moderate gaps.",
|
||||
"picker_description": "Customizes Standard Mode for balanced driving.",
|
||||
@@ -1250,7 +1246,6 @@
|
||||
},
|
||||
{
|
||||
"key": "RelaxedPersonalityProfile",
|
||||
"requires_offroad": true,
|
||||
"label": "Relaxed",
|
||||
"description": "Customize the \"Relaxed\" personality profile. Designed for smoother, more comfortable driving with larger gaps.",
|
||||
"picker_description": "Customizes Relaxed Mode for smoother driving.",
|
||||
@@ -1262,7 +1257,6 @@
|
||||
},
|
||||
{
|
||||
"key": "TrafficFollow",
|
||||
"requires_offroad": true,
|
||||
"label": "Following Distance",
|
||||
"description": "The minimum following distance to the lead vehicle. openpilot blends between this value and the \"Relaxed\" profile as speed increases. Increase for more space; decrease for tighter gaps.",
|
||||
"data_type": "float",
|
||||
@@ -1275,7 +1269,6 @@
|
||||
},
|
||||
{
|
||||
"key": "TrafficJerkAcceleration",
|
||||
"requires_offroad": true,
|
||||
"label": "Acceleration Smoothness",
|
||||
"description": "How smoothly openpilot accelerates. Increase for gentler starts; decrease for faster but more abrupt takeoffs.",
|
||||
"data_type": "float",
|
||||
@@ -1288,7 +1281,6 @@
|
||||
},
|
||||
{
|
||||
"key": "TrafficJerkDeceleration",
|
||||
"requires_offroad": true,
|
||||
"label": "Braking Smoothness",
|
||||
"description": "How smoothly openpilot brakes. Increase for gentler stops; decrease for quicker but sharper braking.",
|
||||
"data_type": "float",
|
||||
@@ -1301,7 +1293,6 @@
|
||||
},
|
||||
{
|
||||
"key": "TrafficJerkDanger",
|
||||
"requires_offroad": true,
|
||||
"label": "Safety Gap Bias",
|
||||
"description": "How much extra space openpilot keeps from the vehicle ahead. Increase for larger gaps and more cautious following; decrease for tighter gaps and closer following.",
|
||||
"data_type": "float",
|
||||
@@ -1314,7 +1305,6 @@
|
||||
},
|
||||
{
|
||||
"key": "TrafficJerkSpeedDecrease",
|
||||
"requires_offroad": true,
|
||||
"label": "Slowdown Response",
|
||||
"description": "How smoothly openpilot slows down. Increase for more gradual deceleration; decrease for faster but sharper slowdowns.",
|
||||
"data_type": "float",
|
||||
@@ -1327,7 +1317,6 @@
|
||||
},
|
||||
{
|
||||
"key": "TrafficJerkSpeed",
|
||||
"requires_offroad": true,
|
||||
"label": "Speed-Up Response",
|
||||
"description": "How smoothly openpilot speeds up. Increase for more gradual acceleration; decrease for quicker but more jolting acceleration.",
|
||||
"data_type": "float",
|
||||
@@ -1340,7 +1329,6 @@
|
||||
},
|
||||
{
|
||||
"key": "AggressiveFollow",
|
||||
"requires_offroad": true,
|
||||
"label": "Following Distance",
|
||||
"description": "How many seconds openpilot follows behind lead vehicles when using the \"Aggressive\" profile. Increase for more space; decrease for tighter gaps.\n\nDefault: 1.25 seconds.",
|
||||
"data_type": "float",
|
||||
@@ -1353,7 +1341,6 @@
|
||||
},
|
||||
{
|
||||
"key": "AggressiveFollowHigh",
|
||||
"requires_offroad": true,
|
||||
"label": "High Speed Following Distance",
|
||||
"description": "Following distance for higher speeds in the \"Aggressive\" profile. openpilot smoothly blends from the base value to this value as speed rises.",
|
||||
"data_type": "float",
|
||||
@@ -1366,7 +1353,6 @@
|
||||
},
|
||||
{
|
||||
"key": "AggressiveJerkAcceleration",
|
||||
"requires_offroad": true,
|
||||
"label": "Acceleration Smoothness",
|
||||
"description": "How smoothly openpilot accelerates. Increase for gentler starts; decrease for faster but more abrupt takeoffs.",
|
||||
"data_type": "float",
|
||||
@@ -1379,7 +1365,6 @@
|
||||
},
|
||||
{
|
||||
"key": "AggressiveJerkDeceleration",
|
||||
"requires_offroad": true,
|
||||
"label": "Braking Smoothness",
|
||||
"description": "How smoothly openpilot brakes. Increase for gentler stops; decrease for quicker but sharper braking.",
|
||||
"data_type": "float",
|
||||
@@ -1392,7 +1377,6 @@
|
||||
},
|
||||
{
|
||||
"key": "AggressiveJerkDanger",
|
||||
"requires_offroad": true,
|
||||
"label": "Safety Gap Bias",
|
||||
"description": "How much extra space openpilot keeps from the vehicle ahead. Increase for larger gaps and more cautious following; decrease for tighter gaps and closer following.",
|
||||
"data_type": "float",
|
||||
@@ -1405,7 +1389,6 @@
|
||||
},
|
||||
{
|
||||
"key": "AggressiveJerkSpeedDecrease",
|
||||
"requires_offroad": true,
|
||||
"label": "Slowdown Response",
|
||||
"description": "How smoothly openpilot slows down. Increase for more gradual deceleration; decrease for faster but sharper slowdowns.",
|
||||
"data_type": "float",
|
||||
@@ -1418,7 +1401,6 @@
|
||||
},
|
||||
{
|
||||
"key": "AggressiveJerkSpeed",
|
||||
"requires_offroad": true,
|
||||
"label": "Speed-Up Response",
|
||||
"description": "How smoothly openpilot speeds up. Increase for more gradual acceleration; decrease for quicker but more jolting acceleration.",
|
||||
"data_type": "float",
|
||||
@@ -1431,7 +1413,6 @@
|
||||
},
|
||||
{
|
||||
"key": "StandardFollow",
|
||||
"requires_offroad": true,
|
||||
"label": "Following Distance",
|
||||
"description": "How many seconds openpilot follows behind lead vehicles when using the \"Standard\" profile. Increase for more space; decrease for tighter gaps.\n\nDefault: 1.45 seconds.",
|
||||
"data_type": "float",
|
||||
@@ -1444,7 +1425,6 @@
|
||||
},
|
||||
{
|
||||
"key": "StandardFollowHigh",
|
||||
"requires_offroad": true,
|
||||
"label": "High Speed Following Distance",
|
||||
"description": "Following distance for higher speeds in the \"Standard\" profile. openpilot smoothly blends from the base value to this value as speed rises.",
|
||||
"data_type": "float",
|
||||
@@ -1457,7 +1437,6 @@
|
||||
},
|
||||
{
|
||||
"key": "StandardJerkAcceleration",
|
||||
"requires_offroad": true,
|
||||
"label": "Acceleration Smoothness",
|
||||
"description": "How smoothly openpilot accelerates. Increase for gentler starts; decrease for faster but more abrupt takeoffs.",
|
||||
"data_type": "float",
|
||||
@@ -1470,7 +1449,6 @@
|
||||
},
|
||||
{
|
||||
"key": "StandardJerkDeceleration",
|
||||
"requires_offroad": true,
|
||||
"label": "Braking Smoothness",
|
||||
"description": "How smoothly openpilot brakes. Increase for gentler stops; decrease for quicker but sharper braking.",
|
||||
"data_type": "float",
|
||||
@@ -1483,7 +1461,6 @@
|
||||
},
|
||||
{
|
||||
"key": "StandardJerkDanger",
|
||||
"requires_offroad": true,
|
||||
"label": "Safety Gap Bias",
|
||||
"description": "How much extra space openpilot keeps from the vehicle ahead. Increase for larger gaps and more cautious following; decrease for tighter gaps and closer following.",
|
||||
"data_type": "float",
|
||||
@@ -1496,7 +1473,6 @@
|
||||
},
|
||||
{
|
||||
"key": "StandardJerkSpeedDecrease",
|
||||
"requires_offroad": true,
|
||||
"label": "Slowdown Response",
|
||||
"description": "How smoothly openpilot slows down. Increase for more gradual deceleration; decrease for faster but sharper slowdowns.",
|
||||
"data_type": "float",
|
||||
@@ -1509,7 +1485,6 @@
|
||||
},
|
||||
{
|
||||
"key": "StandardJerkSpeed",
|
||||
"requires_offroad": true,
|
||||
"label": "Speed-Up Response",
|
||||
"description": "How smoothly openpilot speeds up. Increase for more gradual acceleration; decrease for quicker but more jolting acceleration.",
|
||||
"data_type": "float",
|
||||
@@ -1522,7 +1497,6 @@
|
||||
},
|
||||
{
|
||||
"key": "RelaxedFollow",
|
||||
"requires_offroad": true,
|
||||
"label": "Following Distance",
|
||||
"description": "How many seconds openpilot follows behind lead vehicles when using the \"Relaxed\" profile. Increase for more space; decrease for tighter gaps.\n\nDefault: 1.6 seconds.",
|
||||
"data_type": "float",
|
||||
@@ -1535,7 +1509,6 @@
|
||||
},
|
||||
{
|
||||
"key": "RelaxedFollowHigh",
|
||||
"requires_offroad": true,
|
||||
"label": "High Speed Following Distance",
|
||||
"description": "Following distance for higher speeds in the \"Relaxed\" profile. openpilot smoothly blends from the base value to this value as speed rises.",
|
||||
"data_type": "float",
|
||||
@@ -1548,7 +1521,6 @@
|
||||
},
|
||||
{
|
||||
"key": "RelaxedJerkAcceleration",
|
||||
"requires_offroad": true,
|
||||
"label": "Acceleration Smoothness",
|
||||
"description": "How smoothly openpilot accelerates. Increase for gentler starts; decrease for faster but more abrupt takeoffs.",
|
||||
"data_type": "float",
|
||||
@@ -1561,7 +1533,6 @@
|
||||
},
|
||||
{
|
||||
"key": "RelaxedJerkDeceleration",
|
||||
"requires_offroad": true,
|
||||
"label": "Braking Smoothness",
|
||||
"description": "How smoothly openpilot brakes. Increase for gentler stops; decrease for quicker but sharper braking.",
|
||||
"data_type": "float",
|
||||
@@ -1574,7 +1545,6 @@
|
||||
},
|
||||
{
|
||||
"key": "RelaxedJerkDanger",
|
||||
"requires_offroad": true,
|
||||
"label": "Safety Gap Bias",
|
||||
"description": "How much extra space openpilot keeps from the vehicle ahead. Increase for larger gaps and more cautious following; decrease for tighter gaps and closer following.",
|
||||
"data_type": "float",
|
||||
@@ -1587,7 +1557,6 @@
|
||||
},
|
||||
{
|
||||
"key": "RelaxedJerkSpeedDecrease",
|
||||
"requires_offroad": true,
|
||||
"label": "Slowdown Response",
|
||||
"description": "How smoothly openpilot slows down. Increase for more gradual deceleration; decrease for faster but sharper slowdowns.",
|
||||
"data_type": "float",
|
||||
@@ -1600,7 +1569,6 @@
|
||||
},
|
||||
{
|
||||
"key": "RelaxedJerkSpeed",
|
||||
"requires_offroad": true,
|
||||
"label": "Speed-Up Response",
|
||||
"description": "How smoothly openpilot speeds up. Increase for more gradual acceleration; decrease for quicker but more jolting acceleration.",
|
||||
"data_type": "float",
|
||||
|
||||
@@ -101,7 +101,7 @@ def test_disabling_without_a_stored_document_does_not_create_one():
|
||||
assert lpp.synchronise_profile_document_enabled(None, False, ev_tuning=False, truck_tuning=False) is None
|
||||
|
||||
|
||||
def test_every_state_affecting_personality_param_is_parked_only():
|
||||
def test_every_state_affecting_personality_param_is_included_in_bulk_restore_parked_guard():
|
||||
assert lpp.PERSONALITY_PARKED_PARAM_KEYS == (
|
||||
lpp.PERSONALITY_ADVANCED_PARAM_KEYS
|
||||
| lpp.PERSONALITY_FOLLOW_PARAM_KEYS
|
||||
|
||||
@@ -124,6 +124,24 @@ const PERSONALITY_ADVANCED_KEYS = {
|
||||
standard: ["StandardJerkAcceleration", "StandardJerkDeceleration", "StandardJerkDanger", "StandardJerkSpeedDecrease", "StandardJerkSpeed"],
|
||||
relaxed: ["RelaxedJerkAcceleration", "RelaxedJerkDeceleration", "RelaxedJerkDanger", "RelaxedJerkSpeedDecrease", "RelaxedJerkSpeed"],
|
||||
}
|
||||
const PERSONALITY_EDITOR_PARAM_KEYS = new Set([
|
||||
"CustomPersonalities",
|
||||
...PERSONALITY_DEFINITIONS.map(profile => personalityProfileParamKey(profile.id)),
|
||||
...Object.values(PERSONALITY_ADVANCED_KEYS).flat(),
|
||||
"TrafficFollow", "AggressiveFollow", "AggressiveFollowHigh", "StandardFollow", "StandardFollowHigh",
|
||||
"RelaxedFollow", "RelaxedFollowHigh",
|
||||
])
|
||||
function parseRoadFlag(value) {
|
||||
if (value === true || value === 1 || value === "1" || value === "true" || value === "True") return true
|
||||
if (value === false || value === 0 || value === "" || value === "0" || value === "false" || value === "False") return false
|
||||
return null
|
||||
}
|
||||
|
||||
function personalityRoadStateKnown() {
|
||||
const onroad = parseRoadFlag(state.values.IsOnroad)
|
||||
const offroad = parseRoadFlag(state.values.IsOffroad)
|
||||
return onroad !== null && offroad !== null && onroad !== offroad
|
||||
}
|
||||
const PANDA_FIRMWARE_TOGGLE_KEYS = new Set(["IgnoreIgnitionLine", "RemoteStartBootsComma", "HKGRemoteStartBootsComma"])
|
||||
const FLM_ADVANCED_LATERAL_KEYS = new Set([
|
||||
"AdvancedLateralTune", "ForceAutoTune", "ForceAutoTuneOff", "UseAutoSteerDelay", "SteerDelay",
|
||||
@@ -896,6 +914,13 @@ async function refreshCscCalibrationValues() {
|
||||
const nextValues = { ...state.values }
|
||||
let changed = false
|
||||
for (const [key, value] of entries) {
|
||||
if (value === null && (key === "IsOnroad" || key === "IsOffroad")) {
|
||||
if (nextValues[key] !== null) {
|
||||
nextValues[key] = null
|
||||
changed = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (value === null || nextValues[key] === value) continue
|
||||
nextValues[key] = value
|
||||
changed = true
|
||||
@@ -930,7 +955,7 @@ async function refreshUiContextValues() {
|
||||
if (uiContextPollInflight || state.loadingValues) return uiContextPollInflight
|
||||
|
||||
uiContextPollInflight = Promise.all(
|
||||
["IsOnroad", "IsMetric"].map(async key => {
|
||||
["IsOnroad", "IsOffroad", "IsMetric"].map(async key => {
|
||||
const response = await fetch(`/api/params?key=${encodeURIComponent(key)}`, { cache: "no-store" })
|
||||
if (!response.ok) return [key, null]
|
||||
const raw = (await response.text()).trim().toLowerCase()
|
||||
@@ -949,7 +974,13 @@ async function refreshUiContextValues() {
|
||||
state.values = nextValues
|
||||
scheduleSyncInputs()
|
||||
}
|
||||
}).catch(() => {}).finally(async () => {
|
||||
}).catch(() => {
|
||||
const nextValues = { ...state.values, IsOnroad: null, IsOffroad: null }
|
||||
if (state.values.IsOnroad !== null || state.values.IsOffroad !== null) {
|
||||
state.values = nextValues
|
||||
scheduleSyncInputs()
|
||||
}
|
||||
}).finally(async () => {
|
||||
await fetchLongitudinalMode()
|
||||
uiContextPollInflight = null
|
||||
})
|
||||
@@ -1579,12 +1610,15 @@ function getSettingLockReason(param) {
|
||||
if (param?.key === "CustomPersonalities" && state.personalityMigrationRequired) {
|
||||
return "This profile data requires a verified migration before it can be edited."
|
||||
}
|
||||
if (PERSONALITY_EDITOR_PARAM_KEYS.has(param?.key) && !personalityRoadStateKnown()) {
|
||||
return "Driving state is not confirmed. Refresh before editing personalities."
|
||||
}
|
||||
if (param?.key === LONGITUDINAL_MODE_KEY) {
|
||||
if (state.longitudinalModeUpdating) return "Updating longitudinal control mode…"
|
||||
if (!state.longitudinalMode) return "Longitudinal mode state unavailable. Refresh to retry."
|
||||
return state.longitudinalMode.locked ? state.longitudinalMode.reason : ""
|
||||
}
|
||||
if (param?.requires_offroad && state.values.IsOnroad) {
|
||||
if (param?.requires_offroad && (!personalityRoadStateKnown() || state.values.IsOnroad)) {
|
||||
return "This setting can only be changed while parked."
|
||||
}
|
||||
if (param?.requires_parked && !state.values.VehicleParked && !(param.key === "ForceOffroad" && state.values.ForceOffroad)) {
|
||||
@@ -1701,9 +1735,9 @@ async function recoverPersonalitySave() {
|
||||
(config.preset === "custom" && config.curve.length !== data.speed_breakpoints_mph?.[category]?.length)) throw new Error("Saved profiles are malformed.")
|
||||
}
|
||||
}
|
||||
const onroad = [false, "", "0", "False", "false"].includes(values?.IsOnroad) ? false : [true, "1", "True", "true"].includes(values?.IsOnroad) ? true : null
|
||||
const offroad = [true, "1", "True", "true"].includes(values?.IsOffroad)
|
||||
if (onroad === null || (!onroad && !offroad)) throw new Error("Road state could not be verified.")
|
||||
const onroad = parseRoadFlag(values?.IsOnroad)
|
||||
const offroad = parseRoadFlag(values?.IsOffroad)
|
||||
if (onroad === null || offroad === null || onroad === offroad) throw new Error("Road state could not be verified.")
|
||||
state.values = { ...state.values, IsOnroad: onroad, IsOffroad: offroad }
|
||||
state.personalityProfiles = data.profiles
|
||||
state.personalityMigrationRequired = !!data.migration_required
|
||||
@@ -1721,8 +1755,10 @@ async function recoverPersonalitySave() {
|
||||
}
|
||||
}
|
||||
|
||||
async function savePersonalityCategory(profileId, category, preset, curve, successMessage) {
|
||||
if (state.values.IsOnroad) return false
|
||||
async function savePersonalityCategory(profileId, category, preset, curve, successMessage, expectedOnroad = null) {
|
||||
if (!personalityRoadStateKnown()) return false
|
||||
if (expectedOnroad === null) expectedOnroad = parseRoadFlag(state.values.IsOnroad)
|
||||
if (expectedOnroad !== null && parseRoadFlag(state.values.IsOnroad) !== expectedOnroad) return false
|
||||
if (!window.location.pathname.startsWith("/device_settings") || state.personalityProfilesError || state.personalityProfilesLoading) return false
|
||||
if (state.personalityMigrationRequired) {
|
||||
showParamSnackbar("This profile data requires a verified migration before it can be edited.", "error")
|
||||
@@ -1735,7 +1771,8 @@ async function savePersonalityCategory(profileId, category, preset, curve, succe
|
||||
state.personalityUpdating = { ...state.personalityUpdating, [updateKey]: true }
|
||||
try {
|
||||
if (uiContextPollInflight) await uiContextPollInflight
|
||||
if (generation !== personalityViewGeneration || !window.location.pathname.startsWith("/device_settings") || state.values.IsOnroad || state.personalityMigrationRequired) return false
|
||||
if (generation !== personalityViewGeneration || !window.location.pathname.startsWith("/device_settings") || !personalityRoadStateKnown() ||
|
||||
(expectedOnroad !== null && parseRoadFlag(state.values.IsOnroad) !== expectedOnroad) || state.personalityMigrationRequired) return false
|
||||
const response = await fetch("/api/personality_profiles", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
@@ -1971,9 +2008,10 @@ function beginPersonalityCurveDrag(event, profileId, category) {
|
||||
const config = state.personalityProfiles?.[profileId]?.[category]
|
||||
const bounds = state.personalityMeta?.bounds?.[category]
|
||||
const definition = PERSONALITY_CATEGORY_DEFINITIONS[category]
|
||||
if (state.personalityMigrationRequired || !(canvas instanceof HTMLCanvasElement) || !config || !bounds || !definition || state.personalityUpdating[personalityUpdateKey(profileId, category)]) return
|
||||
if (!personalityRoadStateKnown() || state.personalityMigrationRequired || !(canvas instanceof HTMLCanvasElement) || !config || !bounds || !definition || state.personalityUpdating[personalityUpdateKey(profileId, category)]) return
|
||||
|
||||
event.preventDefault()
|
||||
const startingOnroad = parseRoadFlag(state.values.IsOnroad)
|
||||
const curve = [...config.curve]
|
||||
const geometry = graphGeometry(category, curve, canvas.clientWidth || 660)
|
||||
const chartRect = canvas.getBoundingClientRect()
|
||||
@@ -2000,7 +2038,7 @@ function beginPersonalityCurveDrag(event, profileId, category) {
|
||||
}
|
||||
const finish = async pointerEvent => {
|
||||
removeListeners(pointerEvent)
|
||||
const saved = await savePersonalityCategory(profileId, category, "custom", curve, `${definition.label} graph updated.`)
|
||||
const saved = await savePersonalityCategory(profileId, category, "custom", curve, `${definition.label} graph updated.`, startingOnroad)
|
||||
if (!saved && !state.personalityProfilesError && window.location.pathname.startsWith("/device_settings")) restorePersonalityCurveVisual(profileId, category, state.personalityProfiles[profileId][category].curve)
|
||||
}
|
||||
const cancel = pointerEvent => {
|
||||
@@ -2070,7 +2108,7 @@ function renderPersonalityCurve(profile, category, config) {
|
||||
<h4>Custom ${definition.label}</h4>
|
||||
</div>
|
||||
<div class="ds-personality-curve-actions">
|
||||
<button type="button" class="ds-reset-btn" aria-label="Reset ${profile.label} ${definition.label} graph to Dom default" disabled="${() => !!state.values.IsOnroad || !!state.personalityMigrationRequired || !!state.personalityUpdating[updateKey]}" @click="${() => resetPersonalityCurve(profile.id, category)}">Reset</button>
|
||||
<button type="button" class="ds-reset-btn" aria-label="Reset ${profile.label} ${definition.label} graph to Dom default" disabled="${() => !personalityRoadStateKnown() || !!state.personalityMigrationRequired || !!state.personalityUpdating[updateKey]}" @click="${() => resetPersonalityCurve(profile.id, category)}">Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
${config.curve.some(value => value > Number(editBounds[1])) ? html`
|
||||
@@ -2084,8 +2122,8 @@ function renderPersonalityCurve(profile, category, config) {
|
||||
height="${geometry.height}"
|
||||
role="img"
|
||||
aria-label="${definition.title} by speed with the ${profile.label} reference shown faintly. Drag near a point to adjust it."
|
||||
aria-disabled="${() => !!state.values.IsOnroad || !!state.personalityMigrationRequired}"
|
||||
@pointerdown="${event => { if (!state.values.IsOnroad && !state.personalityMigrationRequired) beginPersonalityCurveDrag(event, profile.id, category) }}"></canvas>
|
||||
aria-disabled="${() => !personalityRoadStateKnown() || !!state.personalityMigrationRequired}"
|
||||
@pointerdown="${event => { if (personalityRoadStateKnown() && !state.personalityMigrationRequired) beginPersonalityCurveDrag(event, profile.id, category) }}"></canvas>
|
||||
<div class="ds-personality-values">
|
||||
${config.curve.map((value, index) => html`
|
||||
<label class="ds-personality-value">
|
||||
@@ -2100,7 +2138,7 @@ function renderPersonalityCurve(profile, category, config) {
|
||||
aria-describedby="personality-curve-error-${profile.id}-${category}"
|
||||
aria-invalid="${() => state.personalityCurveErrors[updateKey] ? "true" : "false"}"
|
||||
value="${Number(value).toFixed(2)}"
|
||||
disabled="${() => !!state.values.IsOnroad || !!state.personalityMigrationRequired || !!state.personalityUpdating[updateKey]}"
|
||||
disabled="${() => !personalityRoadStateKnown() || !!state.personalityMigrationRequired || !!state.personalityUpdating[updateKey]}"
|
||||
@change="${event => adjustPersonalityCurvePoint(profile.id, category, index, event.currentTarget)}" />
|
||||
<b id="personality-value-${profile.id}-${category}-${index}">${Number(value).toFixed(2)} ${definition.valueUnit}</b>
|
||||
</label>
|
||||
@@ -2133,7 +2171,7 @@ function renderPersonalityCategoryField(profile, category, config) {
|
||||
type="button"
|
||||
class="ds-personality-option"
|
||||
aria-pressed="${() => config.preset === option ? "true" : "false"}"
|
||||
disabled="${() => !!state.values.IsOnroad || !!state.personalityMigrationRequired || !!state.personalityUpdating[updateKey]}"
|
||||
disabled="${() => !personalityRoadStateKnown() || !!state.personalityMigrationRequired || !!state.personalityUpdating[updateKey]}"
|
||||
@click="${() => updatePersonalityPreset(profile.id, category, option)}">
|
||||
${personalityPresetLabel(option)}
|
||||
</button>
|
||||
@@ -2187,7 +2225,7 @@ function personalityAdvancedOptions(key) {
|
||||
}
|
||||
|
||||
function updatePersonalityAdvancedPreset(param, mode) {
|
||||
if (state.values.IsOnroad || state.numericUpdating[param.key]) return
|
||||
if (!personalityRoadStateKnown() || state.numericUpdating[param.key]) return
|
||||
if (mode === "custom") {
|
||||
state.personalityAdvancedCustomOpen = { ...state.personalityAdvancedCustomOpen, [param.key]: true }
|
||||
return
|
||||
@@ -2214,7 +2252,7 @@ function renderPersonalityAdvancedValue(profile, param) {
|
||||
class="ds-personality-option ds-personality-advanced-choice"
|
||||
aria-label="${profile.label} ${param.label} ${label} percentage preset"
|
||||
aria-pressed="${() => personalityAdvancedMode(param.key) === mode ? "true" : "false"}"
|
||||
disabled="${() => !!state.values.IsOnroad || !!state.numericUpdating[param.key]}"
|
||||
disabled="${() => !personalityRoadStateKnown() || !!state.numericUpdating[param.key]}"
|
||||
@click="${() => updatePersonalityAdvancedPreset(param, mode)}">${label}</button>
|
||||
`)}
|
||||
</div>
|
||||
@@ -2226,7 +2264,7 @@ function renderPersonalityAdvancedValue(profile, param) {
|
||||
step="${bounds.step}"
|
||||
aria-label="${profile.label} ${param.label} custom percentage"
|
||||
value="${() => resolveCurrentNumericValue(param, bounds)}"
|
||||
disabled="${() => !!state.values.IsOnroad || !!state.numericUpdating[param.key]}"
|
||||
disabled="${() => !personalityRoadStateKnown() || !!state.numericUpdating[param.key]}"
|
||||
@change="${event => updateNumericParam(param, event.currentTarget.value, event.currentTarget)}" />
|
||||
<span>${bounds.min}–${bounds.max}</span>
|
||||
</label>
|
||||
@@ -2305,10 +2343,12 @@ function renderPersonalityProfilesPanel() {
|
||||
if (!state.personalityMeta) return html`<div class="ds-personality-error" role="alert" aria-live="assertive">Driving personalities could not be loaded. Refresh the page to retry.</div>`
|
||||
return html`
|
||||
<div class="ds-personality-profiles" id="personality-profiles-panel">
|
||||
${() => !personalityRoadStateKnown() ? html`<div class="ds-personality-migration-warning" role="status">Driving state is not confirmed. Personality editing is temporarily disabled.</div>` : ""}
|
||||
${() => parseRoadFlag(state.values.IsOnroad) === true && personalityRoadStateKnown() ? html`<div class="ds-personality-migration-warning" role="note">Changes to the active profile can take effect immediately and alter acceleration, braking, or following behavior. Make adjustments only when it is safe, and stay ready to take control.</div>` : ""}
|
||||
${() => state.personalityMigrationRequired ? html`
|
||||
<div class="ds-personality-migration-warning" role="alert" aria-live="assertive">
|
||||
<span>This profile data requires a verified migration before it can be edited.</span>
|
||||
<button type="button" class="ds-reset-btn" disabled="${() => !!state.values.IsOnroad || state.personalityMigrationInProgress}" @click="${migratePersonalityProfiles}">
|
||||
<button type="button" class="ds-reset-btn" disabled="${() => parseRoadFlag(state.values.IsOnroad) === true || !personalityRoadStateKnown() || state.personalityMigrationInProgress}" @click="${migratePersonalityProfiles}">
|
||||
${() => state.personalityMigrationInProgress ? "Migrating..." : "Migrate profiles"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,12 @@ import { formatProfileSpeed, profileSpeedUnit, personalityProfileParamKey } from
|
||||
const PROFILES = ["traffic", "aggressive", "standard", "relaxed"]
|
||||
const CATEGORIES = { acceleration: "Acceleration", braking: "Braking", following: "Following" }
|
||||
|
||||
function roadFlag(value) {
|
||||
if (value === true || value === 1 || value === "1" || value === "true" || value === "True") return true
|
||||
if (value === false || value === 0 || value === "" || value === "0" || value === "false" || value === "False") return false
|
||||
return null
|
||||
}
|
||||
|
||||
export const PersonalityProfiles = {
|
||||
name: "PersonalityProfiles",
|
||||
props: { manageOpen: { default: null } },
|
||||
@@ -15,8 +21,13 @@ export const PersonalityProfiles = {
|
||||
},
|
||||
computed: {
|
||||
expanded: { get() { return this.manageOpen ?? this.localExpanded }, set(value) { this.localExpanded = value; this.$emit("manage") } },
|
||||
offroad() { return [false, "", "0", "False", "false"].includes(this.values.IsOnroad) && [true, "1", "True", "true"].includes(this.values.IsOffroad) },
|
||||
locked() { return !this.ready || this.busy || !this.offroad },
|
||||
isOnroad() { return roadFlag(this.values.IsOnroad) === true },
|
||||
roadStateKnown() {
|
||||
const onroad = roadFlag(this.values.IsOnroad)
|
||||
const offroad = roadFlag(this.values.IsOffroad)
|
||||
return onroad !== null && offroad !== null && onroad !== offroad
|
||||
},
|
||||
locked() { return !this.ready || this.busy || !this.roadStateKnown },
|
||||
editingLocked() { return this.locked || this.curvePending || !!this.data?.migration_required },
|
||||
},
|
||||
async mounted() {
|
||||
@@ -118,14 +129,22 @@ export const PersonalityProfiles = {
|
||||
this.contextRequest = api.getParams()
|
||||
const values = await this.contextRequest
|
||||
if (this.disposed) return
|
||||
const previousOnroad = roadFlag(this.values.IsOnroad)
|
||||
const nextOnroad = roadFlag(values?.IsOnroad)
|
||||
if (!this.busy) this.values = values
|
||||
if (!this.offroad) { this.drag = null; this.drafts = {}; this.curveText = {} }
|
||||
if (!this.roadStateKnown || (previousOnroad !== null && nextOnroad !== previousOnroad)) {
|
||||
this.drag = null
|
||||
this.drafts = {}
|
||||
this.curveText = {}
|
||||
}
|
||||
} catch (e) { this.ready = false; this.error = "Connection lost. Reconnecting…" }
|
||||
finally { this.contextPending = false; this.contextRequest = null }
|
||||
},
|
||||
async write(action, check = () => !this.editingLocked) {
|
||||
if (!this.roadStateKnown) return false
|
||||
const startingOnroad = this.isOnroad
|
||||
if (this.contextPending) { try { await this.contextRequest } catch { return } }
|
||||
if (this.disposed || !check()) return
|
||||
if (this.disposed || !this.roadStateKnown || this.isOnroad !== startingOnroad || !check()) return
|
||||
this.busy = true
|
||||
this.error = ""
|
||||
this.notice = ""
|
||||
@@ -148,7 +167,7 @@ export const PersonalityProfiles = {
|
||||
} finally { this.busy = false }
|
||||
if (!this.disposed) await this.load()
|
||||
},
|
||||
migrate() { return this.write(() => api.migratePersonalityProfiles(), () => !this.locked) },
|
||||
migrate() { return this.write(() => api.migratePersonalityProfiles(), () => !this.locked && !this.isOnroad) },
|
||||
toggle(key, event) {
|
||||
const value = event.target.checked
|
||||
event.target.checked = this.enabled(this.values[key])
|
||||
@@ -179,29 +198,32 @@ 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 startingOnroad = this.isOnroad
|
||||
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, ...(reset ? { reset: true } : {}), expected: this.data.profiles[profile][category] }), () => !this.locked && !this.data?.migration_required)) this.notice = ""
|
||||
if (this.disposed || !this.roadStateKnown || this.isOnroad !== startingOnroad) return
|
||||
if (await this.write(() => api.savePersonalityProfile({ profile, category, preset: "custom", curve: snapshot, ...(reset ? { reset: true } : {}), expected: this.data.profiles[profile][category] }), () => !this.locked && this.isOnroad === startingOnroad && !this.data?.migration_required)) this.notice = ""
|
||||
} finally {
|
||||
this.discard(profile, category)
|
||||
this.curvePending = false
|
||||
}
|
||||
},
|
||||
async setAdvanced(param, raw) {
|
||||
if (!this.roadStateKnown) return
|
||||
const startingOnroad = this.isOnroad
|
||||
if (this.contextPending) { try { await this.contextRequest } catch { return } }
|
||||
if (this.disposed) return
|
||||
if (this.disposed || !this.roadStateKnown || this.isOnroad !== startingOnroad) return
|
||||
const value = Number(raw)
|
||||
const { min, max, step } = this.bounds(param)
|
||||
if (String(raw).trim() === "" || !Number.isFinite(value) || value < min || value > max || Math.abs((value - min) / step - Math.round((value - min) / step)) > 1e-7) {
|
||||
this.advancedErrors[param.key] = `Enter ${min}–${max}% in increments of ${step}.`; return
|
||||
}
|
||||
delete this.advancedErrors[param.key]
|
||||
return this.write(() => api.updateParam({ key: param.key, value }), () => !this.paramLocked(param.key))
|
||||
return this.write(() => api.updateParam({ key: param.key, value }), () => !this.paramLocked(param.key) && this.isOnroad === startingOnroad)
|
||||
},
|
||||
graphMax(profile, category) {
|
||||
if (this.drag?.profile === profile && this.drag.category === category) return this.drag.max
|
||||
@@ -277,10 +299,11 @@ export const PersonalityProfiles = {
|
||||
<p v-if="busy" role="status" class="gx-personalities__live">Saving…</p>
|
||||
<p v-if="!data && !error" role="status">Loading profiles…</p>
|
||||
<template v-if="data">
|
||||
<p v-if="!offroad" role="note">Active driving personality can be switched on-road. Saved profile tuning is available off-road.</p>
|
||||
<p v-if="!roadStateKnown" role="note">Driving state is not confirmed. Editing is temporarily disabled.</p>
|
||||
<p v-else-if="isOnroad" role="note">Changes to the active profile can take effect immediately and alter acceleration, braking, or following behavior. Make adjustments only when it is safe, and stay ready to take control.</p>
|
||||
<div v-if="data.migration_required" role="alert" class="gx-personalities__error">
|
||||
<p>Stored profiles need migration before editing.</p>
|
||||
<button type="button" class="gx-btn" :disabled="locked" @click="migrate">Migrate profiles</button>
|
||||
<button type="button" class="gx-btn" :disabled="locked || isOnroad" @click="migrate">Migrate profiles</button>
|
||||
</div>
|
||||
<p v-if="!enabled(values.CustomPersonalities)">Enable to configure profiles. Existing defaults remain active while off.</p>
|
||||
<div class="gx-personalities__grid">
|
||||
|
||||
@@ -50,7 +50,12 @@ module.exports = async ({page, data, values, writes, errors, output}) => {
|
||||
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());
|
||||
const onroadReset = profile.getByRole('button',{name:/reset to default/i}).first();
|
||||
assert(await onroadReset.isEnabled());
|
||||
const writesBeforeOnroadReset = writes.length;
|
||||
await onroadReset.click();
|
||||
await settled();
|
||||
assert.equal(writes.length, writesBeforeOnroadReset + 1);
|
||||
assert.deepEqual(errors,[]);
|
||||
console.log('PASS: rendered reset controls, retained presets, reload persistence, per-category defaults/reference parity, narrow/wide layout and onroad lock');
|
||||
console.log('PASS: rendered reset controls, retained presets, reload persistence, per-category defaults/reference parity, narrow/wide layout and onroad editing');
|
||||
};
|
||||
|
||||
@@ -18,10 +18,10 @@ module.exports=async({page,curve,data,values,faults,counts,errors})=>{
|
||||
let release=gate('params');await poll();const n=curve.locator('input').nth(2);await n.fill('1.65');assert.equal(await n.inputValue(),'1.65');assert(await n.isEnabled());
|
||||
let before=counts().attempts;await n.press('Tab');assert.equal(counts().attempts,before);
|
||||
assert(await n.isDisabled());release();await idle();assert.equal(counts().attempts,before+1);assert.equal(data.profiles.traffic.acceleration.curve[2],1.65);results.push('numeric pending poll commits once');
|
||||
// Off-road state changing while the change waits must prevent the PUT.
|
||||
// Road state changing while the change waits must prevent the PUT.
|
||||
release=gate('params');await poll();before=counts().attempts;await edit('1.7');values.IsOnroad='True';values.IsOffroad='';release();
|
||||
await page.waitForFunction(()=>!document.querySelector('#app').__vue_app__._instance.proxy.curvePending);
|
||||
assert.equal(counts().attempts,before);assert.equal(await n.inputValue(),'1.65');assert(await n.isDisabled());results.push('pending change rechecks offroad');
|
||||
assert.equal(counts().attempts,before);assert.equal(await n.inputValue(),'1.65');assert(await n.isEnabled());results.push('pending change rechecks road state');
|
||||
values.IsOnroad='';values.IsOffroad='True';await poll();await idle();
|
||||
// In-flight PUT blocks repeat authoring until verified readback.
|
||||
release=gate('put');before=counts().attempts;await edit('1.75');await page.waitForFunction(()=>document.querySelector('#app').__vue_app__._instance.proxy.busy);
|
||||
|
||||
@@ -27,7 +27,7 @@ module.exports = async ({page, data, values, faults, counts, errors}) => {
|
||||
// A road-state change during the read must reject the queued click.
|
||||
release = await gate(); attempts = counts().attempts;
|
||||
await custom.click(); values.IsOnroad='True'; values.IsOffroad=''; release(); await idle();
|
||||
assert.equal(counts().attempts, attempts); assert(await custom.isDisabled());
|
||||
assert.equal(counts().attempts, attempts); assert(await custom.isEnabled());
|
||||
values.IsOnroad=''; values.IsOffroad='True';
|
||||
await page.evaluate(async () => await document.querySelector('#app').__vue_app__._instance.proxy.refreshContext());
|
||||
// Multiple clicks waiting for one poll cannot produce overlapping PUTs.
|
||||
@@ -42,5 +42,5 @@ module.exports = async ({page, data, values, faults, counts, errors}) => {
|
||||
await page.evaluate(() => document.querySelector('#app').__vue_app__.unmount());
|
||||
release(); await page.waitForTimeout(100); assert.equal(counts().attempts, attempts);
|
||||
assert.deepEqual(errors, []);
|
||||
console.log('PASS: stable appearance across 3 polls; deferred click saves once; road transition blocks; pending PUT blocks overlaps; unmount cancels.');
|
||||
console.log('PASS: stable appearance across 3 polls; deferred click saves once; road transition cancels queued write; pending PUT blocks overlaps; unmount cancels.');
|
||||
};
|
||||
|
||||
@@ -123,11 +123,17 @@ const output=process.env.PERSONALITY_BROWSER_OUTPUT || path.join(require('os').t
|
||||
await page.waitForFunction(()=>document.querySelector('.gx-personalities__advanced .gx-personalities__category input')===null);
|
||||
assert.equal(await row.getByRole('button',{name:'Chill',exact:true}).getAttribute('aria-pressed'),'true');
|
||||
values.IsOnroad='True';values.IsOffroad='';
|
||||
await page.waitForFunction(()=>document.querySelector('.gx-personalities__options button').disabled);
|
||||
await page.waitForFunction(()=>!document.querySelector('.gx-personalities__options button').disabled);
|
||||
assert.deepEqual(errors,[]);
|
||||
values.IsOnroad='';values.IsOffroad='True';
|
||||
await page.waitForFunction(()=>!document.querySelector('.gx-personalities__options button').disabled);
|
||||
console.log('PASS: real API road-state encodings unlock parked controls; Custom graph opens; no Refresh; advanced inputs Custom-only; on-road tuning guard retained.');
|
||||
values.IsOffroad=null;
|
||||
await page.evaluate(async()=>await document.querySelector('#app').__vue_app__._instance.proxy.refreshContext());
|
||||
await page.waitForFunction(()=>document.querySelector('.gx-personalities__options button').disabled);
|
||||
values.IsOffroad='True';
|
||||
await page.evaluate(async()=>await document.querySelector('#app').__vue_app__._instance.proxy.refreshContext());
|
||||
await page.waitForFunction(()=>!document.querySelector('.gx-personalities__options button').disabled);
|
||||
console.log('PASS: personality controls remain editable on-road; Custom graph opens; no Refresh; advanced inputs Custom-only.');
|
||||
assert.deepEqual(errors,[]);
|
||||
console.log('PASS: four profile cards; no horizontal overflow at 320/390/768/1280; Custom delegates initial curve to backend; no browser errors. Synthetic API only.');
|
||||
const results=[];
|
||||
|
||||
@@ -27,13 +27,14 @@ window.fetch=async (input, init={}) => {
|
||||
throw new Error('Unmocked request: '+method+' '+url.pathname);
|
||||
};
|
||||
paramsFixture.IsOnroad = ${process.env.GALAXY_DOM_ONROAD === '1'};
|
||||
paramsFixture.IsOffroad = ${process.env.GALAXY_DOM_ONROAD !== '1'};
|
||||
paramsFixture.GalaxyDeveloperMode = true;
|
||||
const profilesFixture = ${JSON.stringify(JSON.parse(fs.readFileSync(path.join(__dirname,'browser/fixtures/personality_profiles.json'))))};
|
||||
for (const [id,profile] of Object.entries(profilesFixture.profiles)) {
|
||||
for (const [category,config] of Object.entries(profile)) {config.preset='custom';config.curve=[...profilesFixture.reference_curves[id][category]];}
|
||||
}
|
||||
window.profileWrites=[];
|
||||
paramsFixture.IsOffroad=true;paramsFixture.CustomPersonalities=true;
|
||||
paramsFixture.CustomPersonalities=true;
|
||||
for(const key of ['TrafficPersonalityProfile','AggressivePersonalityProfile','StandardPersonalityProfile','RelaxedPersonalityProfile'])paramsFixture[key]=true;
|
||||
const baseFetch=window.fetch;
|
||||
window.fetch=async(input,init={})=>{
|
||||
|
||||
@@ -89,6 +89,28 @@ def test_galaxy_new_ui_is_the_visible_default_choice():
|
||||
assert "Galaxy (old)" in galaxy_default["description"]
|
||||
|
||||
|
||||
def test_driving_personality_controls_are_not_parked_only():
|
||||
params = {
|
||||
param["key"]: param
|
||||
for section in _layout()
|
||||
for param in section.get("params", [])
|
||||
}
|
||||
personality_keys = {
|
||||
"CustomPersonalities",
|
||||
*{f"{profile}PersonalityProfile" for profile in ("Traffic", "Aggressive", "Standard", "Relaxed")},
|
||||
"TrafficFollow", "AggressiveFollow", "AggressiveFollowHigh", "StandardFollow", "StandardFollowHigh",
|
||||
"RelaxedFollow", "RelaxedFollowHigh",
|
||||
*{
|
||||
f"{profile}{suffix}"
|
||||
for profile in ("Traffic", "Aggressive", "Standard", "Relaxed")
|
||||
for suffix in ("JerkAcceleration", "JerkDeceleration", "JerkDanger", "JerkSpeedDecrease", "JerkSpeed")
|
||||
},
|
||||
}
|
||||
|
||||
assert personality_keys <= params.keys()
|
||||
assert all(params[key].get("requires_offroad") is not True for key in personality_keys)
|
||||
|
||||
|
||||
def test_brake_status_toggle_is_galaxy_only():
|
||||
setting = _params_by_section(_layout())["Visual (Display & UI)"]["ShowBrakeStatus"]
|
||||
|
||||
|
||||
@@ -62,11 +62,13 @@ def test_malformed_reset_never_writes(monkeypatch, extra):
|
||||
assert params.writes == []
|
||||
|
||||
|
||||
def test_reset_obeys_offroad_and_stale_editor_guards(monkeypatch):
|
||||
def test_reset_and_stale_editor_guards_allow_onroad_editing(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 == []
|
||||
response = client.put("/api/personality_profiles", json=payload)
|
||||
assert response.status_code == 200
|
||||
assert params.writes
|
||||
params.writes.clear()
|
||||
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
|
||||
|
||||
@@ -210,19 +210,25 @@ def test_saved_v2_high_curve_read_migrate_edit_and_master_round_trip(monkeypatch
|
||||
|
||||
|
||||
@pytest.mark.parametrize("state", [{"IsOnroad": True}, {"IsOnroad": False, "IsOffroad": False}])
|
||||
def test_saved_v2_high_curve_never_bypasses_parked_write_guard(monkeypatch, state):
|
||||
def test_saved_v2_high_curve_is_editable_onroad_but_migration_needs_offroad(monkeypatch, state):
|
||||
profiles = default_personality_profiles(False)
|
||||
profiles["aggressive"]["acceleration"] = {"preset": "custom", "curve": [6.0] * 10}
|
||||
raw = json.dumps(profile_document(profiles, enabled=True))
|
||||
client, params = _client(monkeypatch, {PERSONALITY_PROFILES_PARAM: raw, **state})
|
||||
assert client.get("/api/personality_profiles").status_code == 200
|
||||
assert client.put("/api/personality_profiles", json={
|
||||
edit = client.put("/api/personality_profiles", json={
|
||||
"profile": "aggressive", "category": "acceleration", "preset": "custom", "curve": [3.0] + [6.0] * 9,
|
||||
}).status_code == 403
|
||||
})
|
||||
assert client.post("/api/personality_profiles/migrate").status_code == 403
|
||||
assert client.put("/api/params", json={"key": "CustomPersonalities", "value": False}).status_code == 403
|
||||
assert params.values[PERSONALITY_PROFILES_PARAM] == raw
|
||||
assert params.writes == []
|
||||
if state["IsOnroad"]:
|
||||
assert edit.status_code == 200
|
||||
assert client.put("/api/params", json={"key": "CustomPersonalities", "value": False}).status_code == 200
|
||||
assert strict_profile_document(params.values[PERSONALITY_PROFILES_PARAM])["profiles"]["aggressive"]["acceleration"]["curve"] == [3.0] + [6.0] * 9
|
||||
else:
|
||||
assert edit.status_code == 403
|
||||
assert client.put("/api/params", json={"key": "CustomPersonalities", "value": False}).status_code == 403
|
||||
assert params.values[PERSONALITY_PROFILES_PARAM] == raw
|
||||
assert params.writes == []
|
||||
|
||||
|
||||
def test_get_returns_disabled_standard_defaults_and_explicit_graph_metadata(monkeypatch):
|
||||
@@ -528,22 +534,27 @@ def test_api_exposes_and_enforces_requested_acceleration_and_braking_bounds(monk
|
||||
{"IsOnroad": True, "IsOffroad": False},
|
||||
{"IsOnroad": False, "IsOffroad": False},
|
||||
])
|
||||
def test_dedicated_and_generic_profile_mutations_require_confirmed_offroad(monkeypatch, device_state):
|
||||
def test_dedicated_profile_edits_allow_confirmed_onroad_but_reject_unknown_state(monkeypatch, device_state):
|
||||
original = profile_document(default_personality_profiles(False), enabled=False)
|
||||
client, params = _client(monkeypatch, {**device_state, PERSONALITY_PROFILES_PARAM: original})
|
||||
before = json.loads(json.dumps(params.values))
|
||||
|
||||
dedicated = client.put("/api/personality_profiles", json={
|
||||
"profile": "traffic", "category": "acceleration", "preset": "eco", "curve": [1.0] * 7,
|
||||
"profile": "traffic", "category": "acceleration", "preset": "eco", "curve": [1.0] * 10,
|
||||
})
|
||||
generic = client.put("/api/params", json={"key": PERSONALITY_PROFILES_PARAM, "value": {"enabled": True}})
|
||||
legacy_parent = client.put("/api/params", json={"key": "CustomPersonalities", "value": True})
|
||||
|
||||
assert dedicated.status_code == 403
|
||||
assert generic.status_code == 403
|
||||
assert legacy_parent.status_code == 403
|
||||
assert "parked" in legacy_parent.get_json()["error"].lower()
|
||||
assert params.values == before
|
||||
if device_state["IsOnroad"]:
|
||||
assert dedicated.status_code == 200
|
||||
legacy_parent = client.put("/api/params", json={"key": "CustomPersonalities", "value": True})
|
||||
assert legacy_parent.status_code == 200
|
||||
assert params.get_bool("CustomPersonalities") is True
|
||||
else:
|
||||
assert dedicated.status_code == 403
|
||||
legacy_parent = client.put("/api/params", json={"key": "CustomPersonalities", "value": True})
|
||||
assert legacy_parent.status_code == 403
|
||||
assert "state" in legacy_parent.get_json()["error"].lower()
|
||||
assert params.values[PERSONALITY_PROFILES_PARAM] == original
|
||||
|
||||
|
||||
def test_generic_profile_mutation_is_also_rejected_while_parked(monkeypatch):
|
||||
@@ -553,6 +564,41 @@ def test_generic_profile_mutation_is_also_rejected_while_parked(monkeypatch):
|
||||
assert PERSONALITY_PROFILES_PARAM not in params.values
|
||||
|
||||
|
||||
def test_dedicated_profile_write_rechecks_road_state_before_commit(monkeypatch):
|
||||
client, params = _client(monkeypatch, {"IsOnroad": True})
|
||||
original_update = the_galaxy.update_personality_profile
|
||||
|
||||
def transition_during_validation(*args, **kwargs):
|
||||
result = original_update(*args, **kwargs)
|
||||
params.values.update(IsOnroad=False, IsOffroad=False)
|
||||
return result
|
||||
|
||||
monkeypatch.setattr(the_galaxy, "update_personality_profile", transition_during_validation)
|
||||
response = client.put("/api/personality_profiles", json={
|
||||
"profile": "standard", "category": "acceleration", "preset": "eco", "curve": [1.0] * 10,
|
||||
})
|
||||
|
||||
assert response.status_code == 403
|
||||
assert params.writes == []
|
||||
assert "state" in response.get_json()["error"].lower()
|
||||
|
||||
|
||||
def test_advanced_personality_write_rechecks_road_state_before_commit(monkeypatch):
|
||||
client, params = _client(monkeypatch, {"IsOnroad": True})
|
||||
checks = 0
|
||||
|
||||
def transition_after_validation():
|
||||
nonlocal checks
|
||||
checks += 1
|
||||
return checks > 1
|
||||
|
||||
monkeypatch.setattr(the_galaxy, "_personality_editor_write_locked", transition_after_validation)
|
||||
response = client.put("/api/params", json={"key": "StandardJerkAcceleration", "value": 75})
|
||||
|
||||
assert response.status_code == 403
|
||||
assert params.writes == []
|
||||
|
||||
|
||||
def test_dedicated_enable_mutation_is_rejected(monkeypatch):
|
||||
client, params = _client(monkeypatch, {"IsOnroad": False})
|
||||
response = client.put("/api/personality_profiles", json={"enabled": True})
|
||||
@@ -604,7 +650,7 @@ def test_master_toggle_writes_in_fail_closed_order(monkeypatch, enabled, expecte
|
||||
|
||||
@pytest.mark.parametrize("enabled", [False, True])
|
||||
@pytest.mark.parametrize("road_change", [{"IsOnroad": True}, {"IsOffroad": False}])
|
||||
def test_master_toggle_rechecks_parked_state_after_waiting_for_profile_lock(monkeypatch, enabled, road_change):
|
||||
def test_master_toggle_rechecks_confirmed_road_state_after_waiting_for_profile_lock(monkeypatch, enabled, road_change):
|
||||
original = profile_document(default_personality_profiles(False), enabled=not enabled)
|
||||
client, params = _client(monkeypatch, {
|
||||
"CustomPersonalities": not enabled, PERSONALITY_PROFILES_PARAM: original,
|
||||
@@ -651,7 +697,11 @@ def test_profile_document_write_failure_never_enables_master(monkeypatch):
|
||||
|
||||
def test_unverified_profile_document_write_never_enables_master(monkeypatch):
|
||||
client, params = _client(monkeypatch, {"IsOnroad": False, "CustomPersonalities": False})
|
||||
monkeypatch.setattr(the_galaxy, "_safe_params_get_live_raw", lambda key, default=None, block=False: None)
|
||||
monkeypatch.setattr(
|
||||
the_galaxy,
|
||||
"_safe_params_get_live_raw",
|
||||
lambda key, default=None, block=False: None if key == PERSONALITY_PROFILES_PARAM else params.values.get(key, default),
|
||||
)
|
||||
|
||||
response = client.put("/api/params", json={"key": "CustomPersonalities", "value": True})
|
||||
|
||||
@@ -683,19 +733,27 @@ def test_master_write_failure_leaves_master_false_after_verified_document_write(
|
||||
assert document is not None and document["enabled"] is True
|
||||
|
||||
|
||||
def test_every_state_affecting_personality_write_is_rejected_onroad(monkeypatch):
|
||||
def test_every_state_affecting_personality_write_is_allowed_onroad(monkeypatch):
|
||||
client, params = _client(monkeypatch, {"IsOnroad": True})
|
||||
before = json.loads(json.dumps(params.values))
|
||||
|
||||
for key in PERSONALITY_PARKED_PARAM_KEYS:
|
||||
value = False if key in PERSONALITY_PROFILE_ENABLE_PARAM_KEYS or key == "CustomPersonalities" else 50
|
||||
value = (False if key in PERSONALITY_PROFILE_ENABLE_PARAM_KEYS or key == "CustomPersonalities"
|
||||
else 1.25 if key in PERSONALITY_FOLLOW_PARAM_KEYS else 50)
|
||||
response = client.put("/api/params", json={"key": key, "value": value})
|
||||
assert response.status_code == 403, key
|
||||
assert params.values == before
|
||||
assert response.status_code == 200, key
|
||||
if key in PERSONALITY_PROFILE_ENABLE_PARAM_KEYS or key == "CustomPersonalities":
|
||||
assert params.get_bool(key) is value, key
|
||||
else:
|
||||
assert float(params.values[key]) == value, key
|
||||
|
||||
|
||||
def test_every_state_affecting_personality_write_is_rejected_until_offroad_is_confirmed(monkeypatch):
|
||||
client, params = _client(monkeypatch, {"IsOnroad": False, "IsOffroad": False})
|
||||
@pytest.mark.parametrize("device_state", [
|
||||
{"IsOnroad": False, "IsOffroad": False},
|
||||
{"IsOnroad": False, "IsOffroad": None},
|
||||
{"IsOnroad": True, "IsOffroad": None},
|
||||
])
|
||||
def test_every_state_affecting_personality_write_is_rejected_when_road_state_is_unconfirmed(monkeypatch, device_state):
|
||||
client, params = _client(monkeypatch, device_state)
|
||||
before = json.loads(json.dumps(params.values))
|
||||
|
||||
for key in PERSONALITY_PARKED_PARAM_KEYS:
|
||||
|
||||
@@ -124,10 +124,10 @@ def test_drag_on_expanded_saved_curve_uses_plot_scale_but_caps_only_edited_point
|
||||
assert result["original"] == [6, 4] + [1] * 8
|
||||
|
||||
|
||||
def test_rendered_editor_has_parked_locks_units_and_all_three_profile_categories():
|
||||
def test_rendered_editor_has_confirmed_road_state_locks_units_and_all_three_profile_categories():
|
||||
source = DEVICE_SETTINGS_PATH.read_text(encoding="utf-8")
|
||||
assert 'disabled="${() => !!state.values.IsOnroad' in source
|
||||
assert 'aria-disabled="${() => !!state.values.IsOnroad || !!state.personalityMigrationRequired}"' in source
|
||||
assert 'disabled="${() => !personalityRoadStateKnown()' in source
|
||||
assert 'aria-disabled="${() => !personalityRoadStateKnown() || !!state.personalityMigrationRequired}"' in source
|
||||
assert "profileSpeedUnit" in source
|
||||
assert "m/s²" in source
|
||||
for category in ("acceleration", "braking", "following"):
|
||||
@@ -144,7 +144,7 @@ def test_acceleration_and_braking_presets_render_from_weakest_to_strongest():
|
||||
assert 'braking: ["eco", "standard", "sport", "custom"]' in source
|
||||
|
||||
|
||||
def test_profile_master_and_advanced_controls_declare_parked_only_metadata():
|
||||
def test_profile_master_and_advanced_controls_are_editable_onroad():
|
||||
layout = json.loads(DEVICE_SETTINGS_LAYOUT_PATH.read_text(encoding="utf-8"))
|
||||
params = {param["key"]: param for section in layout for param in section.get("params", [])}
|
||||
keys = {
|
||||
@@ -163,7 +163,7 @@ def test_profile_master_and_advanced_controls_declare_parked_only_metadata():
|
||||
for suffix in ("JerkAcceleration", "JerkDeceleration", "JerkDanger", "JerkSpeedDecrease", "JerkSpeed")
|
||||
},
|
||||
}
|
||||
assert all(params[key].get("requires_offroad") is True for key in keys)
|
||||
assert all(params[key].get("requires_offroad") is not True for key in keys)
|
||||
|
||||
|
||||
def test_profile_errors_are_escaped_before_the_legacy_html_snackbar_sink():
|
||||
@@ -203,8 +203,9 @@ def test_each_personality_card_maps_to_its_persisted_enable_toggle():
|
||||
]
|
||||
|
||||
|
||||
def test_each_personality_card_exposes_an_accessible_parked_only_enable_toggle():
|
||||
def test_each_personality_card_exposes_an_accessible_enable_toggle():
|
||||
source = DEVICE_SETTINGS_PATH.read_text(encoding="utf-8")
|
||||
assert "PERSONALITY_EDITOR_PARAM_KEYS.has(param?.key) && !personalityRoadStateKnown()" in source
|
||||
assert "function renderPersonalityProfileToggle" in source
|
||||
toggle = source.split("function renderPersonalityProfileToggle", 1)[1].split("\n}", 1)[0]
|
||||
assert "personalityProfileParamKey(profile.id)" in toggle
|
||||
@@ -445,7 +446,7 @@ def test_schema_migration_state_is_visible_and_blocks_profile_writes():
|
||||
assert "Migrate profiles" in source
|
||||
migration_warning = source.split('class="ds-personality-migration-warning"', 1)[1].split("</div>", 1)[0]
|
||||
assert '!state.values.IsOffroad' not in migration_warning
|
||||
assert '!!state.values.IsOnroad || state.personalityMigrationInProgress' in migration_warning
|
||||
assert 'parseRoadFlag(state.values.IsOnroad) === true || !personalityRoadStateKnown() || state.personalityMigrationInProgress' in source
|
||||
css = DEVICE_SETTINGS_CSS_PATH.read_text(encoding="utf-8")
|
||||
assert ".ds-personality-migration-warning" in css
|
||||
|
||||
@@ -487,7 +488,7 @@ def test_device_settings_polls_driving_state_and_units_while_visible():
|
||||
source = DEVICE_SETTINGS_PATH.read_text(encoding="utf-8")
|
||||
assert "function ensureUiContextPolling" in source
|
||||
refresh = source.split("async function refreshUiContextValues", 1)[1].split("\n}", 1)[0]
|
||||
assert '["IsOnroad", "IsMetric"]' in refresh
|
||||
assert '["IsOnroad", "IsOffroad", "IsMetric"]' in refresh
|
||||
assert '`/api/params?key=${encodeURIComponent(key)}`' in refresh
|
||||
polling = source.split("function ensureUiContextPolling", 1)[1].split("\n}", 1)[0]
|
||||
assert 'document.visibilityState === "visible"' in polling
|
||||
@@ -616,16 +617,25 @@ def test_personality_responsive_layout_uses_available_card_width():
|
||||
assert 'context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0)' in source
|
||||
|
||||
|
||||
def test_personality_save_blocks_onroad_even_for_synthetic_events():
|
||||
def test_personality_save_allows_confirmed_onroad_and_blocks_unknown_state():
|
||||
source = DEVICE_SETTINGS_PATH.read_text(encoding="utf-8")
|
||||
road_state = "function parseRoadFlag" + source.split("function parseRoadFlag", 1)[1].split("\n}\n", 1)[0] + "\n}"
|
||||
known = "function personalityRoadStateKnown" + source.split("function personalityRoadStateKnown", 1)[1].split("\n}\n", 1)[0] + "\n}"
|
||||
save = "async function savePersonalityCategory" + source.split("async function savePersonalityCategory", 1)[1].split("\n}\n", 1)[0] + "\n}"
|
||||
result = _run_node("""
|
||||
const state = {values:{IsOnroad:true}};
|
||||
const fetch = () => {throw new Error("On-road write attempted")};
|
||||
""" + save + """
|
||||
console.log(JSON.stringify(await savePersonalityCategory("standard", "acceleration", "eco", [])));
|
||||
const state = {values:{IsOnroad:true,IsOffroad:false},personalityProfiles:{standard:{acceleration:{preset:"standard",curve:[]}}},personalityUpdating:{},personalityProfilesError:"",personalityProfilesLoading:false,personalityMigrationRequired:false};
|
||||
const window = {location:{pathname:"/device_settings"}};
|
||||
let writes = 0;
|
||||
const fetch = async () => { writes++; return {ok:true,json:async()=>({profiles:{standard:{acceleration:{preset:"eco",curve:[]}}}})}; };
|
||||
let uiContextPollInflight = null, personalityViewGeneration = 0;
|
||||
const showParamSnackbar = () => {};
|
||||
""" + road_state + known + save + """
|
||||
const onroad = await savePersonalityCategory("standard", "acceleration", "eco", []);
|
||||
state.values.IsOffroad = "unknown";
|
||||
const uncertain = await savePersonalityCategory("standard", "acceleration", "eco", []);
|
||||
console.log(JSON.stringify({onroad,uncertain,writes}));
|
||||
""")
|
||||
assert result is False
|
||||
assert result == {"onroad": True, "uncertain": False, "writes": 1}
|
||||
|
||||
|
||||
def test_responsive_canvas_keeps_metric_endpoint_labels_separate_and_scales_bitmap():
|
||||
|
||||
@@ -3562,6 +3562,27 @@ def _safe_params_get_bool(key, default=False):
|
||||
def _personality_settings_write_locked():
|
||||
return _safe_params_get_bool("IsOnroad", default=True) or not _safe_params_get_bool("IsOffroad", default=False)
|
||||
|
||||
def _personality_editor_write_locked():
|
||||
def road_state(value):
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode("utf-8", errors="replace")
|
||||
if isinstance(value, str):
|
||||
normalized = value.strip().lower()
|
||||
if normalized in ("1", "true"):
|
||||
return True
|
||||
if normalized in ("0", "false"):
|
||||
return False
|
||||
return None
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
if isinstance(value, int) and value in (0, 1):
|
||||
return bool(value)
|
||||
return None
|
||||
|
||||
is_onroad = road_state(_safe_params_get_live_raw("IsOnroad"))
|
||||
is_offroad = road_state(_safe_params_get_live_raw("IsOffroad"))
|
||||
return is_onroad is None or is_offroad is None or is_onroad == is_offroad
|
||||
|
||||
def _normalize_vasm_config(data):
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError("Configuration must be a JSON object.")
|
||||
@@ -6040,8 +6061,8 @@ def setup(app):
|
||||
profiles = stored_document["profiles"] if configured else default_personality_profiles(ev_tuning, truck_tuning)
|
||||
|
||||
if request.method == "PUT":
|
||||
if _personality_settings_write_locked():
|
||||
return jsonify({"error": "Longitudinal personality profiles can only be changed while off-road."}), 403
|
||||
if _personality_editor_write_locked():
|
||||
return jsonify({"error": "Driving state is unavailable or inconsistent. Refresh before editing personalities."}), 403
|
||||
if current_document is None and stored_document is not None:
|
||||
return jsonify({"error": "Stored longitudinal personality profiles require a verified migration before editing."}), 409
|
||||
data = request.get_json(silent=True)
|
||||
@@ -6088,6 +6109,8 @@ def setup(app):
|
||||
except (KeyError, TypeError, ValueError) as error:
|
||||
return jsonify({"error": str(error)}), 400
|
||||
|
||||
if _personality_editor_write_locked():
|
||||
return jsonify({"error": "Driving state is unavailable or inconsistent. Refresh before editing personalities."}), 403
|
||||
params.put(PERSONALITY_PROFILES_PARAM, profile_document(profiles, enabled=enabled))
|
||||
configured = True
|
||||
migration_required = False
|
||||
@@ -6142,8 +6165,8 @@ def setup(app):
|
||||
key = str(data["key"]).strip()
|
||||
if key.lower() == PERSONALITY_PROFILES_PARAM.lower():
|
||||
return jsonify({"error": "Longitudinal personality profiles must be changed with the Driving Personalities editor."}), 403
|
||||
if key in PERSONALITY_PARKED_PARAM_KEYS and _personality_settings_write_locked():
|
||||
return jsonify({"error": "Driving personality settings can only be changed while parked."}), 403
|
||||
if key in PERSONALITY_PARKED_PARAM_KEYS and _personality_editor_write_locked():
|
||||
return jsonify({"error": "Driving state is unavailable or inconsistent. Refresh before editing personalities."}), 403
|
||||
if key in PERSONALITY_PROFILE_ENABLE_PARAM_KEYS and type(data["value"]) is not bool:
|
||||
return jsonify({"error": f"{key} must be a JSON boolean."}), 400
|
||||
if key in LONGITUDINAL_MODE_KEYS:
|
||||
@@ -6235,8 +6258,8 @@ def setup(app):
|
||||
return jsonify({"error": "CustomPersonalities must be a JSON boolean."}), 400
|
||||
enabled = data["value"]
|
||||
with _PERSONALITY_PROFILES_WRITE_LOCK:
|
||||
if _personality_settings_write_locked():
|
||||
return jsonify({"error": "Driving personality settings can only be changed while parked."}), 403
|
||||
if _personality_editor_write_locked():
|
||||
return jsonify({"error": "Driving state is unavailable or inconsistent. Refresh before editing personalities."}), 403
|
||||
ev_tuning = _get_detected_ev_tuning()
|
||||
truck_tuning = (_get_detected_truck_tuning() or params.get_bool("TruckTuning")) and not ev_tuning
|
||||
raw_document = _safe_params_get_live_raw(PERSONALITY_PROFILES_PARAM)
|
||||
@@ -6245,6 +6268,8 @@ def setup(app):
|
||||
document = synchronise_profile_document_enabled(
|
||||
raw_document, enabled, ev_tuning, truck_tuning,
|
||||
)
|
||||
if _personality_editor_write_locked():
|
||||
return jsonify({"error": "Driving state is unavailable or inconsistent. Refresh before editing personalities."}), 403
|
||||
updated = {"CustomPersonalities": enabled}
|
||||
if enabled:
|
||||
if document is None:
|
||||
@@ -6393,6 +6418,9 @@ def setup(app):
|
||||
except RuntimeError as exc:
|
||||
return jsonify({"error": str(exc)}), 409
|
||||
|
||||
if key in PERSONALITY_PARKED_PARAM_KEYS and _personality_editor_write_locked():
|
||||
return jsonify({"error": "Driving state is unavailable or inconsistent. Refresh before editing personalities."}), 403
|
||||
|
||||
if key in {"LeadIndicator", "HideLeadMarker"}:
|
||||
enabled = str_val.strip() in ("1", "true", "True")
|
||||
if key == "LeadIndicator":
|
||||
|
||||
Reference in New Issue
Block a user