From 979015cd2056983fd2bb4073873c1cf126fc882b Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Sun, 19 Apr 2026 00:59:39 -0500 Subject: [PATCH] Mici customize? --- selfdrive/ui/lib/starpilot_theme.py | 92 ++++++++++++++++++++++ selfdrive/ui/mici/onroad/model_renderer.py | 67 +++++++++++++--- selfdrive/ui/onroad/model_renderer.py | 76 +++++++++++------- selfdrive/ui/onroad/starpilot/path.py | 25 ++---- 4 files changed, 206 insertions(+), 54 deletions(-) create mode 100644 selfdrive/ui/lib/starpilot_theme.py diff --git a/selfdrive/ui/lib/starpilot_theme.py b/selfdrive/ui/lib/starpilot_theme.py new file mode 100644 index 000000000..b244b4803 --- /dev/null +++ b/selfdrive/ui/lib/starpilot_theme.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +import json +from pathlib import Path + +import pyray as rl + +from openpilot.common.basedir import BASEDIR + +ACTIVE_THEME_COLORS_PATH = Path(BASEDIR) / "starpilot/assets/active_theme/colors/colors.json" +STOCK_THEME_COLORS_PATH = Path(BASEDIR) / "starpilot/assets/stock_theme/colors/colors.json" + +_FALLBACK_THEME_COLORS = { + "LaneLines": (255, 255, 255, 178), + "LeadMarker": (201, 34, 49, 255), + "Path": (48, 255, 156, 255), + "PathEdge": (38, 209, 125, 255), + "Sidebar1": (255, 255, 255, 178), + "Sidebar2": (255, 255, 255, 255), + "Sidebar3": (255, 255, 255, 255), +} + +_THEME_COLOR_CACHE: dict[str, object] = { + "stamp": None, + "colors": None, +} + + +def _file_stamp(path: Path) -> tuple[str, int | None, int | None]: + try: + stat = path.stat() + return (str(path), stat.st_mtime_ns, stat.st_size) + except OSError: + return (str(path), None, None) + + +def _clamp_channel(value: object, fallback: int) -> int: + try: + return max(0, min(255, int(value))) + except (TypeError, ValueError): + return fallback + + +def _load_color_map(path: Path) -> dict[str, dict[str, int]]: + try: + with path.open() as f: + data = json.load(f) + if isinstance(data, dict): + return data + except (OSError, ValueError, TypeError): + pass + return {} + + +def _build_color(entry: object, fallback: tuple[int, int, int, int]) -> rl.Color: + if isinstance(entry, dict): + red, green, blue, alpha = fallback + return rl.Color( + _clamp_channel(entry.get("red"), red), + _clamp_channel(entry.get("green"), green), + _clamp_channel(entry.get("blue"), blue), + _clamp_channel(entry.get("alpha"), alpha), + ) + return rl.Color(*fallback) + + +def _load_theme_colors() -> dict[str, rl.Color]: + stamp = (_file_stamp(STOCK_THEME_COLORS_PATH), _file_stamp(ACTIVE_THEME_COLORS_PATH)) + if stamp == _THEME_COLOR_CACHE["stamp"] and _THEME_COLOR_CACHE["colors"] is not None: + return _THEME_COLOR_CACHE["colors"] # type: ignore[return-value] + + stock_colors = _load_color_map(STOCK_THEME_COLORS_PATH) + active_colors = _load_color_map(ACTIVE_THEME_COLORS_PATH) + + colors = {} + for key, fallback in _FALLBACK_THEME_COLORS.items(): + colors[key] = _build_color(active_colors.get(key, stock_colors.get(key)), fallback) + + _THEME_COLOR_CACHE["stamp"] = stamp + _THEME_COLOR_CACHE["colors"] = colors + return colors + + +def get_theme_color(key: str, fallback: rl.Color | None = None) -> rl.Color: + color = _load_theme_colors().get(key) + if color is not None: + return color + return fallback if fallback is not None else rl.WHITE + + +def with_alpha(color: rl.Color, alpha: int) -> rl.Color: + return rl.Color(color.r, color.g, color.b, max(0, min(255, int(alpha)))) diff --git a/selfdrive/ui/mici/onroad/model_renderer.py b/selfdrive/ui/mici/onroad/model_renderer.py index b4112fff7..2f26d248b 100644 --- a/selfdrive/ui/mici/onroad/model_renderer.py +++ b/selfdrive/ui/mici/onroad/model_renderer.py @@ -4,8 +4,10 @@ import pyray as rl from cereal import messaging, car from dataclasses import dataclass, field 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_theme_color, with_alpha 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 @@ -79,7 +81,8 @@ class ModelRenderer(Widget): ) # Get longitudinal control setting from car parameters - if car_params := Params().get("CarParams"): + self._params = Params() + if car_params := self._params.get("CarParams"): cp = messaging.log_from_bytes(car_params, car.CarParams) self._longitudinal_control = cp.openpilotLongitudinalControl @@ -184,21 +187,36 @@ class ModelRenderer(Widget): def _update_model(self, lead, path_x_array): """Update model visualization data based on model message""" + model_ui_enabled = self._params.get_bool("ModelUI", default=True) + if model_ui_enabled: + path_width = self._path_width_to_half_m(self._params.get_float("PathWidth", return_default=True, default=6.1)) + lane_line_width = self._small_distance_to_half_m(self._params.get_float("LaneLinesWidth", return_default=True, default=4.0)) + road_edge_width = self._small_distance_to_half_m(self._params.get_float("RoadEdgesWidth", return_default=True, default=2.0)) + else: + path_width = 0.9 + lane_line_width = 0.025 + road_edge_width = 0.025 + + if model_ui_enabled and self._params.get_bool("DynamicPathWidth", default=False): + if ui_state.status == UIStatus.ENGAGED: + path_width *= 1.0 + elif ui_state.always_on_lateral_active: + path_width *= 0.75 + else: + path_width *= 0.50 + max_distance = np.clip(path_x_array[-1], MIN_DRAW_DISTANCE, MAX_DRAW_DISTANCE) max_idx = self._get_path_length_idx(self._lane_lines[0].raw_points[:, 0], max_distance) # Update lane lines using raw points - line_width_factor = 0.12 for i, lane_line in enumerate(self._lane_lines): - if i in (1, 2): - line_width_factor = 0.16 lane_line.projected_points = self._map_line_to_polygon( - lane_line.raw_points, line_width_factor * self._lane_line_probs[i], 0.0, max_idx + lane_line.raw_points, lane_line_width * self._lane_line_probs[i], 0.0, max_idx ) # Update road edges using raw points for road_edge in self._road_edges: - road_edge.projected_points = self._map_line_to_polygon(road_edge.raw_points, line_width_factor, 0.0, max_idx) + road_edge.projected_points = self._map_line_to_polygon(road_edge.raw_points, road_edge_width, 0.0, max_idx) # Update path using raw points if lead and lead.status: @@ -214,7 +232,7 @@ class ModelRenderer(Widget): high_pass_acceleration = self._acceleration_x_filter.x - self._acceleration_x_filter2.x y_off = np.interp(high_pass_acceleration, [-1, 0, 1], [0.9 * 2, 0.9, 0.9 / 2]) else: - y_off = 0.9 + y_off = path_width max_idx = self._get_path_length_idx(path_x_array, max_distance) self._path.projected_points = self._map_line_to_polygon( @@ -225,7 +243,7 @@ class ModelRenderer(Widget): def _update_experimental_gradient(self): """Pre-calculate experimental mode gradient colors""" - if not self._experimental_mode: + if not (self._experimental_mode or self._params.get_bool("AccelerationPath", default=True)): return max_len = min(len(self._path.projected_points) // 2, len(self._acceleration_x)) @@ -304,7 +322,8 @@ class ModelRenderer(Widget): np.interp(abs(torque), [0.6, 0.8], [0.0, 1.0]) ) else: - color = rl.Color(255, 255, 255, int(alpha * 255)) + lane_lines_color = get_theme_color("LaneLines", rl.WHITE) + color = with_alpha(lane_lines_color, int(alpha * lane_lines_color.a)) return color @@ -334,14 +353,28 @@ class ModelRenderer(Widget): lateral_ui_active = ui_state.status == UIStatus.ENGAGED or ui_state.always_on_lateral_active allow_throttle = sm['longitudinalPlan'].allowThrottle or not self._longitudinal_control or ui_state.always_on_lateral_active self._blend_filter.update(int(allow_throttle)) + custom_theme_selected = (self._params.get("ColorScheme", encoding="utf-8", default="stock") or "stock").lower() != "stock" - if self._experimental_mode: + if self._experimental_mode or self._params.get_bool("AccelerationPath", default=True): # Draw with acceleration coloring if len(self._exp_gradient.colors) > 1: draw_polygon(self._rect, self._path.projected_points, gradient=self._exp_gradient) else: fallback = get_border_color(ui_state) draw_polygon(self._rect, self._path.projected_points, rl.Color(fallback.r, fallback.g, fallback.b, 90)) + elif custom_theme_selected: + path_color = get_theme_color("Path", rl.Color(48, 255, 156, 255)) + gradient = Gradient( + start=(0.0, 1.0), + end=(0.0, 0.0), + colors=[ + with_alpha(path_color, path_color.a), + with_alpha(path_color, int(path_color.a * 0.55)), + with_alpha(path_color, int(path_color.a * 0.10)), + ], + stops=[0.0, 0.5, 1.0], + ) + draw_polygon(self._rect, self._path.projected_points, gradient=gradient) else: # Blend throttle/no throttle colors based on transition blend_factor = round(self._blend_filter.x * 100) / 100 @@ -479,3 +512,17 @@ class ModelRenderer(Widget): int(inv_t * start.b + t * end.b), int(inv_t * start.a + t * end.a) ) for start, end in zip(begin_colors, end_colors, strict=True)] + + def _small_distance_to_half_m(self, value: float) -> float: + if value <= 0: + return 0.0 + if self._params.get_bool("IsMetric"): + return value / 200.0 + return value * (CV.INCH_TO_CM / 100.0) / 2.0 + + def _path_width_to_half_m(self, value: float) -> float: + if value <= 0: + return 0.0 + if self._params.get_bool("IsMetric"): + return value / 2.0 + return value * CV.FOOT_TO_METER / 2.0 diff --git a/selfdrive/ui/onroad/model_renderer.py b/selfdrive/ui/onroad/model_renderer.py index 5818e8b33..20fad2640 100644 --- a/selfdrive/ui/onroad/model_renderer.py +++ b/selfdrive/ui/onroad/model_renderer.py @@ -5,7 +5,9 @@ from cereal import messaging, car from dataclasses import dataclass, field 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_theme_color, with_alpha from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient @@ -185,28 +187,24 @@ class ModelRenderer(Widget): def _update_model(self, lead, path_x_array): """Update model visualization data based on model message""" - path_width = self._params.get_float('PathWidth') # stored in feet, convert to meters - if path_width <= 0: - path_width = 0.9 # default in meters - - lane_line_width = self._params.get_int('LaneLinesWidth') # stored in inches, convert to meters - if lane_line_width <= 0: - lane_line_width = int(0.025 / 0.0254) # default ~1 inch - lane_line_width_m = lane_line_width * 0.0254 - - road_edge_width = self._params.get_int('RoadEdgesWidth') # stored in inches - if road_edge_width <= 0: - road_edge_width = int(0.025 / 0.0254) - road_edge_width_m = road_edge_width * 0.0254 - - path_edge_width_pct = self._params.get_int('PathEdgeWidth') / 100.0 + model_ui_enabled = self._params.get_bool('ModelUI', default=True) + if model_ui_enabled: + path_width = self._path_width_to_half_m(self._params.get_float('PathWidth', return_default=True, default=6.1)) + lane_line_width_m = self._small_distance_to_half_m(self._params.get_float('LaneLinesWidth', return_default=True, default=4.0)) + road_edge_width_m = self._small_distance_to_half_m(self._params.get_float('RoadEdgesWidth', return_default=True, default=2.0)) + path_edge_width_pct = np.clip(self._params.get_float('PathEdgeWidth', return_default=True, default=20.0) / 100.0, 0.0, 1.0) + else: + path_width = 0.9 + lane_line_width_m = 0.025 + road_edge_width_m = 0.025 + path_edge_width_pct = 0.0 # Dynamic path width - if self._params.get_bool('DynamicPathWidth'): + if model_ui_enabled and self._params.get_bool('DynamicPathWidth', default=False): status = ui_state.status if status == UIStatus.ENGAGED: path_width *= 1.0 - elif status == UIStatus.ALWAYS_ON_LATERAL: + elif ui_state.always_on_lateral_active: path_width *= 0.75 else: path_width *= 0.50 @@ -246,7 +244,7 @@ class ModelRenderer(Widget): def _update_experimental_gradient(self): """Pre-calculate experimental mode gradient colors""" - use_acceleration = self._experimental_mode or self._params.get_bool('AccelerationPath') + use_acceleration = self._experimental_mode or self._params.get_bool('AccelerationPath', default=True) use_rainbow = self._params.get_bool('RainbowPath') and not use_acceleration if not use_acceleration and not use_rainbow: @@ -333,19 +331,14 @@ class ModelRenderer(Widget): def _draw_lane_lines(self): """Draw lane lines and road edges""" - color_scheme = self._params.get('ColorScheme', encoding='utf-8') or 'stock' - lane_lines_color = self._params.get('LaneLinesColor', encoding='utf-8') + lane_lines_color = get_theme_color("LaneLines", rl.WHITE) for i, lane_line in enumerate(self._lane_lines): if lane_line.projected_points.size == 0: continue alpha = np.clip(self._lane_line_probs[i], 0.0, 0.7) - if color_scheme != 'stock' and lane_lines_color: - base_color = self._hex_to_color(lane_lines_color) - color = rl.Color(base_color.r, base_color.g, base_color.b, int(alpha * 255)) - else: - color = rl.Color(255, 255, 255, int(alpha * 255)) + color = with_alpha(lane_lines_color, int(alpha * lane_lines_color.a)) draw_polygon(self._rect, lane_line.projected_points, color) for i, road_edge in enumerate(self._road_edges): @@ -364,12 +357,26 @@ class ModelRenderer(Widget): allow_throttle = sm['longitudinalPlan'].allowThrottle or not self._longitudinal_control self._blend_filter.update(int(allow_throttle)) - use_accel_path = self._params.get_bool('AccelerationPath') + use_accel_path = self._params.get_bool('AccelerationPath', default=True) + custom_theme_selected = (self._params.get('ColorScheme', encoding='utf-8', default='stock') or 'stock').lower() != 'stock' if self._experimental_mode or use_accel_path: if len(self._exp_gradient.colors) > 1: draw_polygon(self._rect, self._path.projected_points, gradient=self._exp_gradient) else: draw_polygon(self._rect, self._path.projected_points, rl.Color(255, 255, 255, 30)) + elif custom_theme_selected: + path_color = get_theme_color("Path", rl.Color(48, 255, 156, 255)) + gradient = Gradient( + start=(0.0, 1.0), + end=(0.0, 0.0), + colors=[ + with_alpha(path_color, path_color.a), + with_alpha(path_color, int(path_color.a * 0.55)), + with_alpha(path_color, int(path_color.a * 0.10)), + ], + stops=[0.0, 0.5, 1.0], + ) + draw_polygon(self._rect, self._path.projected_points, gradient=gradient) else: # Blend throttle/no throttle colors based on transition blend_factor = round(self._blend_filter.x * 100) / 100 @@ -384,12 +391,13 @@ class ModelRenderer(Widget): def _draw_lead_indicator(self): # Draw lead vehicles if available + lead_color = get_theme_color("LeadMarker", rl.Color(201, 34, 49, 255)) for lead in self._lead_vehicles: if not lead.glow or not lead.chevron: continue rl.draw_triangle_fan(lead.glow, len(lead.glow), rl.Color(218, 202, 37, 255)) - rl.draw_triangle_fan(lead.chevron, len(lead.chevron), rl.Color(201, 34, 49, lead.fill_alpha)) + rl.draw_triangle_fan(lead.chevron, len(lead.chevron), with_alpha(lead_color, lead.fill_alpha)) def _update_adjacent_paths(self, max_idx: int, max_distance: float): """Compute adjacent lane path polygons by averaging lane line pairs.""" @@ -631,6 +639,20 @@ class ModelRenderer(Widget): int(a * 255) ) + def _small_distance_to_half_m(self, value: float) -> float: + if value <= 0: + return 0.0 + if self._params.get_bool("IsMetric"): + return value / 200.0 + return value * (CV.INCH_TO_CM / 100.0) / 2.0 + + def _path_width_to_half_m(self, value: float) -> float: + if value <= 0: + return 0.0 + if self._params.get_bool("IsMetric"): + return value / 2.0 + return value * CV.FOOT_TO_METER / 2.0 + @staticmethod def _blend_colors(begin_colors, end_colors, t): if t >= 1.0: diff --git a/selfdrive/ui/onroad/starpilot/path.py b/selfdrive/ui/onroad/starpilot/path.py index f0b60b972..dbb6a6e7d 100644 --- a/selfdrive/ui/onroad/starpilot/path.py +++ b/selfdrive/ui/onroad/starpilot/path.py @@ -6,6 +6,7 @@ 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_theme_color, with_alpha from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient @@ -176,20 +177,10 @@ def render_path_edges(renderer) -> None: elif ui_state.traffic_mode_enabled: base_color = rl.Color(201, 34, 49, 241) else: - color_scheme = renderer._params.get("ColorScheme") - if color_scheme and color_scheme.decode("utf-8") != "stock": - path_edges_color = renderer._params.get("PathEdgesColor") - if path_edges_color: - hex_str = path_edges_color.decode("utf-8").lstrip("#") - if len(hex_str) == 6: - r = int(hex_str[0:2], 16) - g = int(hex_str[2:4], 16) - b = int(hex_str[4:6], 16) - base_color = rl.Color(r, g, b, 241) - else: - base_color = rl.Color(23, 134, 68, 241) - else: - base_color = rl.Color(23, 134, 68, 241) + color_scheme = renderer._params.get("ColorScheme", encoding="utf-8", default="stock") + if 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 @@ -198,9 +189,9 @@ def render_path_edges(renderer) -> None: start=(0.0, 1.0), end=(0.0, 0.0), colors=[ - rl.Color(base_color.r, base_color.g, base_color.b, int(255 * 0.4)), - rl.Color(base_color.r, base_color.g, base_color.b, int(255 * 0.35)), - rl.Color(base_color.r, base_color.g, base_color.b, 0), + 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], )