diff --git a/frogpilot/system/the_pond/assets/components/tools/device_settings.js b/frogpilot/system/the_pond/assets/components/tools/device_settings.js
index f04abf2a8..6d1068647 100644
--- a/frogpilot/system/the_pond/assets/components/tools/device_settings.js
+++ b/frogpilot/system/the_pond/assets/components/tools/device_settings.js
@@ -6,6 +6,7 @@ import { html, reactive } from "https://esm.sh/@arrow-js/core"
const state = reactive({
layout: [],
allKeys: [],
+ paramMetaByKey: {},
values: {},
loadingLayout: true,
loadingValues: true,
@@ -34,12 +35,15 @@ async function fetchLayoutAndParams() {
// Extract flatter key map
const keys = []
+ const paramMetaByKey = {}
for (const section of layoutData) {
for (const p of section.params) {
keys.push(p.key)
+ paramMetaByKey[p.key] = p
}
}
state.allKeys = keys
+ state.paramMetaByKey = paramMetaByKey
} catch (e) {
console.error("Failed to fetch UI layout:", e)
}
@@ -58,6 +62,17 @@ async function fetchLayoutAndParams() {
}
function syncInputs() {
+ const selectValue = (value) => (value === null || value === undefined ? "" : String(value))
+ const applySelectOptions = (el, options) => {
+ el.innerHTML = ""
+ for (const opt of options) {
+ const o = document.createElement("option")
+ o.value = String(opt.value)
+ o.textContent = opt.label
+ el.appendChild(o)
+ }
+ }
+
for (const key of state.allKeys) {
const el = document.getElementById(`ds-${key}`)
if (el) {
@@ -65,20 +80,19 @@ function syncInputs() {
el.checked = !!state.values[key]
} else if (el.tagName === "SELECT") {
const endpoint = el.getAttribute("data-endpoint")
+ const inlineOptions = state.paramMetaByKey[key]?.options
if (endpoint && !el.dataset.hydrated) {
el.dataset.hydrated = "1"
fetch(endpoint).then(r => r.json()).then(options => {
- el.innerHTML = ""
- for (const opt of options) {
- const o = document.createElement("option")
- o.value = opt.value
- o.textContent = opt.label
- el.appendChild(o)
- }
- el.value = state.values[key] || ""
+ applySelectOptions(el, options)
+ el.value = selectValue(state.values[key])
}).catch(() => { el.innerHTML = '' })
+ } else if (Array.isArray(inlineOptions) && inlineOptions.length > 0 && !el.dataset.hydrated) {
+ el.dataset.hydrated = "1"
+ applySelectOptions(el, inlineOptions)
+ el.value = selectValue(state.values[key])
} else {
- el.value = state.values[key] || ""
+ el.value = selectValue(state.values[key])
}
} else {
el.value = state.values[key]
@@ -119,6 +133,41 @@ function formatSliderValue(val, stepStr, precisionInt, key) {
return Number(v.toFixed(dec)).toString()
}
+function numericBounds(param) {
+ const defaultBounds = {
+ min: param.min !== undefined ? param.min : (param.data_type === "float" ? 0.0 : 0),
+ max: param.max !== undefined ? param.max : (param.data_type === "float" ? 100.0 : 100),
+ step: param.step !== undefined ? param.step : (param.data_type === "float" ? 0.01 : 1),
+ }
+
+ const toFinite = (value) => {
+ const n = Number(value)
+ return Number.isFinite(n) ? n : null
+ }
+
+ if (param.key === "ScreenBrightness") {
+ return { min: 1, max: 101, step: 1 }
+ }
+ if (param.key === "ScreenBrightnessOnroad") {
+ return { min: 0, max: 101, step: 1 }
+ }
+
+ if (param.key === "SteerKP") {
+ const base = toFinite(state.values.SteerKPStock) || toFinite(state.values.SteerKP) || 0.6
+ return { min: +(base * 0.5).toFixed(2), max: +(base * 1.5).toFixed(2), step: 0.01 }
+ }
+ if (param.key === "SteerLatAccel") {
+ const base = toFinite(state.values.SteerLatAccelStock) || toFinite(state.values.SteerLatAccel) || 2.0
+ return { min: +(base * 0.5).toFixed(2), max: +(base * 1.25).toFixed(2), step: 0.01 }
+ }
+ if (param.key === "SteerRatio") {
+ const base = toFinite(state.values.SteerRatioStock) || toFinite(state.values.SteerRatio) || 15.0
+ return { min: +(base * 0.25).toFixed(2), max: +(base * 1.5).toFixed(2), step: 0.01 }
+ }
+
+ return defaultBounds
+}
+
async function updateParam(key, elType) {
const current = state.values[key]
@@ -161,7 +210,7 @@ function revertInput(key, current, elType) {
const el = document.getElementById(`ds-${key}`)
if (el) {
if (elType === "checkbox") el.checked = !!current
- else if (elType === "dropdown") el.value = current || ""
+ else if (elType === "dropdown") el.value = (current === null || current === undefined ? "" : String(current))
else {
el.value = current
const displayEl = document.getElementById(`ds-display-${key}`)
@@ -255,7 +304,7 @@ export function DeviceSettings() {
if (!state.expanded[p.parent_key]) return ""
}
- const isNumeric = p.ui_type === "numeric" || p.data_type === "float" || p.data_type === "int"
+ const isNumeric = p.ui_type === "numeric"
const isChild = p.parent_key ? "ds-child-modifier" : ""
return html`
@@ -276,18 +325,23 @@ export function DeviceSettings() {
${isNumeric ? html`
+ ${(() => {
+ const bounds = numericBounds(p)
+ return html`
handleSliderInput(e, p.key)}"
@change="${() => updateParam(p.key, 'numeric')}"
/>
+ `
+ })()}
` : p.ui_type === "dropdown" ? html`