diff --git a/selfdrive/ui/lib/starpilot_theme.py b/selfdrive/ui/lib/starpilot_theme.py index 0a07d463d..bf7aa1746 100644 --- a/selfdrive/ui/lib/starpilot_theme.py +++ b/selfdrive/ui/lib/starpilot_theme.py @@ -137,6 +137,13 @@ def get_param_color(params, key: str, fallback_alpha: int = 255) -> rl.Color | N return rl.Color(red, green, blue, alpha) +def is_stock_color_scheme(params) -> bool: + if params is None: + return True + scheme = params.get("ColorScheme", encoding="utf-8", default="stock") + return (scheme or "stock").lower() == "stock" + + def get_visual_color(params, param_key: str, theme_key: str, fallback: rl.Color | None = None) -> rl.Color: base = _as_color(fallback if fallback is not None else rl.WHITE) override = get_param_color(params, param_key, base.a) diff --git a/selfdrive/ui/mici/onroad/model_renderer.py b/selfdrive/ui/mici/onroad/model_renderer.py index 84d72298f..8bb9fce27 100644 --- a/selfdrive/ui/mici/onroad/model_renderer.py +++ b/selfdrive/ui/mici/onroad/model_renderer.py @@ -7,11 +7,11 @@ from openpilot.common.params import Params from openpilot.common.constants import CV from openpilot.common.filter_simple import FirstOrderFilter from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT -from openpilot.selfdrive.ui.lib.starpilot_theme import get_param_color, get_theme_color, get_visual_color, with_alpha +from openpilot.selfdrive.ui.lib.starpilot_theme import get_param_color, get_theme_color, get_visual_color, is_stock_color_scheme, with_alpha from openpilot.selfdrive.ui.lib.starpilot_visuals import lead_indicator_enabled from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus from openpilot.selfdrive.ui.mici.onroad import blend_colors -from openpilot.selfdrive.ui.mici.onroad.starpilot_status import get_border_color, get_path_edge_color +from openpilot.selfdrive.ui.mici.onroad.starpilot_status import get_border_color from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient from openpilot.system.ui.widgets import Widget @@ -21,6 +21,7 @@ MIN_DRAW_DISTANCE = 10.0 MAX_DRAW_DISTANCE = 100.0 RAINBOW_GRADIENT_COLOR_COUNT = 19 RAINBOW_SCROLL_SPEED_DEG_PER_SEC = 60.0 +STOCK_LINE_GREEN = rl.Color(0, 255, 0, 255) THROTTLE_COLORS = [ rl.Color(13, 248, 122, 102), # HSLF(148/360, 0.94, 0.51, 0.4) @@ -370,8 +371,8 @@ class ModelRenderer(Widget): if override is not None: color = with_alpha(override, int(alpha * override.a)) else: - _base_color = get_path_edge_color(ui_state) - color = rl.Color(_base_color.r, _base_color.g, _base_color.b, int(alpha * 255)) + base_color = STOCK_LINE_GREEN if is_stock_color_scheme(self._params) else get_theme_color("PathEdge", STOCK_LINE_GREEN) + color = with_alpha(base_color, int(alpha * base_color.a)) # turn adjacent lls orange if torque is high torque = self._torque_filter.x @@ -383,7 +384,13 @@ class ModelRenderer(Widget): np.interp(abs(torque), [0.6, 0.8], [0.0, 1.0]) ) else: - lane_lines_color = get_visual_color(self._params, "LaneLinesColor", "LaneLines", rl.WHITE) + lane_lines_override = get_param_color(self._params, "LaneLinesColor", STOCK_LINE_GREEN.a) + if lane_lines_override is not None: + lane_lines_color = lane_lines_override + elif is_stock_color_scheme(self._params): + lane_lines_color = STOCK_LINE_GREEN + else: + lane_lines_color = get_theme_color("LaneLines", STOCK_LINE_GREEN) color = with_alpha(lane_lines_color, int(alpha * lane_lines_color.a)) return color diff --git a/selfdrive/ui/onroad/model_renderer.py b/selfdrive/ui/onroad/model_renderer.py index 191ba086f..13aadb799 100644 --- a/selfdrive/ui/onroad/model_renderer.py +++ b/selfdrive/ui/onroad/model_renderer.py @@ -7,7 +7,7 @@ from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params from openpilot.common.constants import CV from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT -from openpilot.selfdrive.ui.lib.starpilot_theme import get_param_color, get_theme_color, get_visual_color, with_alpha +from openpilot.selfdrive.ui.lib.starpilot_theme import get_param_color, get_theme_color, get_visual_color, is_stock_color_scheme, with_alpha from openpilot.selfdrive.ui.lib.starpilot_visuals import lead_indicator_enabled from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus from openpilot.system.ui.lib.application import gui_app @@ -19,6 +19,7 @@ MIN_DRAW_DISTANCE = 10.0 MAX_DRAW_DISTANCE = 100.0 RAINBOW_GRADIENT_COLOR_COUNT = 19 RAINBOW_SCROLL_SPEED_DEG_PER_SEC = 60.0 +STOCK_LINE_GREEN = rl.Color(0, 255, 0, 255) THROTTLE_COLORS = [ rl.Color(13, 248, 122, 102), # HSLF(148/360, 0.94, 0.51, 0.4) @@ -363,7 +364,13 @@ class ModelRenderer(Widget): def _draw_lane_lines(self): """Draw lane lines and road edges""" - lane_lines_color = get_visual_color(self._params, "LaneLinesColor", "LaneLines", rl.WHITE) + lane_lines_override = get_param_color(self._params, "LaneLinesColor", STOCK_LINE_GREEN.a) + if lane_lines_override is not None: + lane_lines_color = lane_lines_override + elif is_stock_color_scheme(self._params): + lane_lines_color = STOCK_LINE_GREEN + else: + lane_lines_color = get_theme_color("LaneLines", STOCK_LINE_GREEN) for i, lane_line in enumerate(self._lane_lines): if lane_line.projected_points.size == 0: diff --git a/selfdrive/ui/onroad/starpilot/path.py b/selfdrive/ui/onroad/starpilot/path.py index 528a6bb61..1a5edb65a 100644 --- a/selfdrive/ui/onroad/starpilot/path.py +++ b/selfdrive/ui/onroad/starpilot/path.py @@ -6,12 +6,13 @@ import numpy as np import pyray as rl from openpilot.selfdrive.ui.lib.starpilot_state import starpilot_state -from openpilot.selfdrive.ui.lib.starpilot_theme import get_param_color, get_theme_color, with_alpha +from openpilot.selfdrive.ui.lib.starpilot_theme import get_param_color, get_theme_color, is_stock_color_scheme, with_alpha from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient _METRICS_FONT = None _METRICS_FONT_SIZE = 45 +_STOCK_LINE_GREEN = rl.Color(0, 255, 0, 241) def _get_metrics_font(): @@ -147,14 +148,10 @@ def render_path_edges(renderer) -> None: Qt reference: paintPathEdges in starpilot_annotated_camera.cc:732-769 Path edges are the area between track_edge_vertices (outer) and track_vertices (inner). - Color is based on current mode: - - If switchback_mode_enabled: use STATUS_SWITCHBACK_MODE_ENABLED color - - If always_on_lateral_active: use STATUS_ALWAYS_ON_LATERAL_ACTIVE color - - If conditional_status == 1: use STATUS_CEM_DISABLED color - - If experimental_mode: use STATUS_EXPERIMENTAL_MODE_ENABLED color - - If traffic_mode_enabled: use STATUS_TRAFFIC_MODE_ENABLED color - - If color_scheme != "stock" and path_edges_color is set: use that color - - Else: stock green gradient HSL(148/360, 0.94, 0.41, 0.4) → HSL(112/360, 1.0, 0.54, 0.35) → transparent + Color selection on Python UIs: + - If path_edges_color is set: use that color + - Else if color_scheme != "stock": use the active theme color + - Else: use a fixed stock-style green so border color carries engagement status The path_edges_color param is a hex string like "#178644". """ @@ -166,48 +163,24 @@ def render_path_edges(renderer) -> None: edge_strip = np.vstack([outer, inner[::-1]]) - if ui_state.switchback_mode_enabled: - base_color = rl.Color(139, 108, 197, 241) - elif ui_state.always_on_lateral_active: - base_color = rl.Color(10, 186, 181, 241) - elif ui_state.conditional_status == 1: - base_color = rl.Color(255, 255, 0, 241) - elif renderer._experimental_mode: - base_color = rl.Color(218, 111, 37, 241) - elif ui_state.traffic_mode_enabled: - base_color = rl.Color(201, 34, 49, 241) + override = get_param_color(renderer._params, "PathEdgesColor", 241) + if override is not None: + base_color = rl.Color(override.r, override.g, override.b, 241) + elif is_stock_color_scheme(renderer._params): + base_color = _STOCK_LINE_GREEN else: - override = get_param_color(renderer._params, "PathEdgesColor", 241) - color_scheme = renderer._params.get("ColorScheme", encoding="utf-8", default="stock") - if override is not None: - base_color = rl.Color(override.r, override.g, override.b, 241) - elif color_scheme != "stock": - theme_color = get_theme_color("PathEdge", rl.Color(23, 134, 68, 241)) - base_color = rl.Color(theme_color.r, theme_color.g, theme_color.b, 241) - else: - base_color = None + theme_color = get_theme_color("PathEdge", _STOCK_LINE_GREEN) + base_color = rl.Color(theme_color.r, theme_color.g, theme_color.b, 241) - if base_color is not None: - gradient = Gradient( - start=(0.0, 1.0), - end=(0.0, 0.0), - colors=[ - with_alpha(base_color, int(255 * 0.4)), - with_alpha(base_color, int(255 * 0.35)), - with_alpha(base_color, 0), - ], - stops=[0.0, 0.5, 1.0], - ) - else: - gradient = Gradient( - start=(0.0, 1.0), - end=(0.0, 0.0), - colors=[ - _hsla_to_color(148.0 / 360.0, 0.94, 0.41, 0.4), - _hsla_to_color(112.0 / 360.0, 1.0, 0.54, 0.35), - _hsla_to_color(112.0 / 360.0, 1.0, 0.54, 0.0), - ], - stops=[0.0, 0.5, 1.0], - ) + gradient = Gradient( + start=(0.0, 1.0), + end=(0.0, 0.0), + colors=[ + with_alpha(base_color, int(255 * 0.4)), + with_alpha(base_color, int(255 * 0.35)), + with_alpha(base_color, 0), + ], + stops=[0.0, 0.5, 1.0], + ) draw_polygon(renderer._rect, edge_strip, gradient=gradient) diff --git a/starpilot/system/the_pond/assets/components/tools/device_settings.js b/starpilot/system/the_pond/assets/components/tools/device_settings.js index 5372bd228..149712d21 100644 --- a/starpilot/system/the_pond/assets/components/tools/device_settings.js +++ b/starpilot/system/the_pond/assets/components/tools/device_settings.js @@ -2,6 +2,11 @@ import { html, reactive } from "/assets/vendor/arrow-core.js" const endpointOptionsCache = {} const endpointOptionsInflight = {} +const COLOR_UI_DEFAULTS = { + LaneLinesColor: "#00ff00", + PathEdgesColor: "#00ff00", + PathColor: "#30ff9c", +} // Plain variables — scheduling/routing flags that must NOT be reactive let syncScheduled = false @@ -42,6 +47,34 @@ function toSelectValue(value) { return value === null || value === undefined ? "" : String(value) } +function normalizeHexColor(rawValue) { + const value = String(rawValue || "").trim() + if (!value || value.toLowerCase() === "stock") return "" + + const stripped = value.startsWith("#") ? value.slice(1) : value + if (!/^[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$/.test(stripped)) return "" + return `#${stripped.slice(0, 6).toLowerCase()}` +} + +function getColorDefault(param) { + const candidate = normalizeHexColor(param?.default_color) + if (candidate) return candidate + return COLOR_UI_DEFAULTS[param?.key] || "#ffffff" +} + +function resolveColorInputValue(param, rawValue = undefined) { + return normalizeHexColor(rawValue ?? state.values[param?.key]) || getColorDefault(param) +} + +function formatColorDisplayValue(param, rawValue = undefined) { + const value = normalizeHexColor(rawValue ?? state.values[param?.key]) + return value ? value.toUpperCase() : "Stock" +} + +function isStockColorValue(rawValue) { + return normalizeHexColor(rawValue) === "" +} + function resolveEndpointTemplate(template) { if (!template) return "" return String(template).replace(/\{([A-Za-z0-9_]+)\}/g, (_, key) => { @@ -126,6 +159,14 @@ function syncInputs() { el.checked = !!state.values[el.id.slice(3)] } + // Sync color inputs — map unset/"stock" values to the picker fallback color. + for (const el of document.querySelectorAll("input[type='color'].ds-color[id^='ds-']")) { + const key = el.id.slice(3) + const param = state.paramMetaByKey[key] + if (!param) continue + el.value = resolveColorInputValue(param) + } + // Sync selects — hydrate options + set value for (const el of document.querySelectorAll("select.ds-select[id^='ds-']")) { const key = el.id.slice(3) @@ -544,6 +585,8 @@ async function updateParam(key, elType) { } else if (elType === "dropdown") { formattedVal = coerceValueByType(el.value, param.data_type) selectedLabel = el.options?.[el.selectedIndex]?.textContent || "" + } else if (elType === "color") { + formattedVal = normalizeHexColor(el.value) || getColorDefault(param) } else { formattedVal = coerceValueByType(el.value, param.data_type) } @@ -589,9 +632,46 @@ function revertInput(key, current, elType) { return } + if (elType === "color") { + const param = state.paramMetaByKey[key] + if (!param) return + el.value = resolveColorInputValue(param, current) + return + } + el.value = current } +async function resetColorParam(param) { + const key = param?.key + if (!key) return + + const current = state.values[key] + if (isStockColorValue(current)) return + + try { + const res = await fetch("/api/params", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ key, value: "stock" }), + }) + const data = await res.json() + + if (res.ok) { + const updated = (data.updated && typeof data.updated === "object") ? data.updated : {} + state.values = { ...state.values, [key]: "stock", ...updated } + showParamSnackbar(data.message || `Parameter '${key}' reset to stock.`) + scheduleSyncInputs() + } else { + showParamSnackbar(data.error || "Failed to reset parameter", "error") + revertInput(key, current, "color") + } + } catch (e) { + showParamSnackbar("Network error — is the device reachable?", "error") + revertInput(key, current, "color") + } +} + function toggleManage(key) { state.expanded = { ...state.expanded, [key]: !state.expanded[key] } scheduleSyncInputs() @@ -640,6 +720,7 @@ function renderSettingRow(p) { } const isNumeric = p.ui_type === "numeric" + const isColor = p.ui_type === "color" const isChild = p.parent_key ? "ds-child-modifier" : "" const lockReason = getSettingLockReason(p) const isLocked = lockReason !== "" @@ -659,7 +740,8 @@ function renderSettingRow(p) { ` : ""} - ${isNumeric ? html`${() => { + ${(isNumeric || isColor) ? html`${() => { + if (isColor) return formatColorDisplayValue(p) const currentValue = state.values[p.key] const bounds = numericBounds(p) return currentValue !== undefined ? formatSliderValue(currentValue, String(bounds.step), p.precision, p.key) : ".." @@ -733,6 +815,20 @@ function renderSettingRow(p) { @change="${() => updateParam(p.key, "dropdown")}"> + ` : p.ui_type === "color" ? html` +
+ + +
` : html`