From c3e4ec630f41c4baa43254a90f718abd1bf764a1 Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Mon, 14 Sep 2026 15:41:25 -0500 Subject: [PATCH] fix --- .../common/assets/device_settings_layout.json | 32 ------ .../test_longitudinal_personality_profiles.py | 2 +- .../components/tools/device_settings.js | 80 ++++++++++---- .../js/components/PersonalityProfiles.js | 45 ++++++-- .../browser/personality_custom_graphs.cjs | 9 +- .../tests/browser/personality_lifecycle.cjs | 4 +- .../tests/browser/personality_poll.cjs | 4 +- .../tests/browser/personality_profiles.cjs | 10 +- .../test_classic_personality_browser.cjs | 3 +- .../tests/test_device_settings_layout.py | 22 ++++ .../test_personality_custom_graphs_api.py | 8 +- .../tests/test_personality_profiles_api.py | 104 ++++++++++++++---- .../tests/test_personality_profiles_js.py | 38 ++++--- starpilot/system/the_galaxy/the_galaxy.py | 40 ++++++- 14 files changed, 282 insertions(+), 119 deletions(-) diff --git a/starpilot/common/assets/device_settings_layout.json b/starpilot/common/assets/device_settings_layout.json index 2ddb93a4cf..0ee696c875 100644 --- a/starpilot/common/assets/device_settings_layout.json +++ b/starpilot/common/assets/device_settings_layout.json @@ -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", diff --git a/starpilot/common/tests/test_longitudinal_personality_profiles.py b/starpilot/common/tests/test_longitudinal_personality_profiles.py index e92b2aa83c..f81a2a28a1 100644 --- a/starpilot/common/tests/test_longitudinal_personality_profiles.py +++ b/starpilot/common/tests/test_longitudinal_personality_profiles.py @@ -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 diff --git a/starpilot/system/the_galaxy/assets/components/tools/device_settings.js b/starpilot/system/the_galaxy/assets/components/tools/device_settings.js index 45e1611481..8e76e51867 100644 --- a/starpilot/system/the_galaxy/assets/components/tools/device_settings.js +++ b/starpilot/system/the_galaxy/assets/components/tools/device_settings.js @@ -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) {

Custom ${definition.label}

- +
${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) }}"> + aria-disabled="${() => !personalityRoadStateKnown() || !!state.personalityMigrationRequired}" + @pointerdown="${event => { if (personalityRoadStateKnown() && !state.personalityMigrationRequired) beginPersonalityCurveDrag(event, profile.id, category) }}">
${config.curve.map((value, index) => html` @@ -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)} @@ -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} `)}
@@ -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)}" /> ${bounds.min}–${bounds.max} @@ -2305,10 +2343,12 @@ function renderPersonalityProfilesPanel() { if (!state.personalityMeta) return html`` return html`
+ ${() => !personalityRoadStateKnown() ? html`
Driving state is not confirmed. Personality editing is temporarily disabled.
` : ""} + ${() => parseRoadFlag(state.values.IsOnroad) === true && personalityRoadStateKnown() ? html`
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.
` : ""} ${() => state.personalityMigrationRequired ? html` diff --git a/starpilot/system/the_galaxy/assets/mobile/js/components/PersonalityProfiles.js b/starpilot/system/the_galaxy/assets/mobile/js/components/PersonalityProfiles.js index e402a7e8c3..742f82de2a 100644 --- a/starpilot/system/the_galaxy/assets/mobile/js/components/PersonalityProfiles.js +++ b/starpilot/system/the_galaxy/assets/mobile/js/components/PersonalityProfiles.js @@ -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 = {

Saving…

Loading profiles…