import os import pyray as rl from cereal import log from openpilot.common.constants import CV from openpilot.selfdrive.ui.mici.onroad import SIDE_PANEL_WIDTH from openpilot.selfdrive.ui.mici.onroad.confidence_ball import ConfidenceBall from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.starpilot.common.experimental_state import CCStatus, CEStatus from openpilot.system.ui.lib.application import FontWeight, gui_app from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.widgets import Widget PERSONALITY_TO_INT = log.LongitudinalPersonality.schema.enumerants DEMO_HOLD_SECONDS = 1.5 DEMO_REASONS = ("chill", "lead", "stop", "curve", "speed") DEMO_PERSONALITIES = ( (False, 0), # aggressive (False, 1), # standard (False, 2), # relaxed (True, 0), # traffic ) WHITE = rl.Color(255, 255, 255, 255) WHITE_DIM = rl.Color(255, 255, 255, 170) BLACK = rl.Color(0, 0, 0, 255) PERSONALITY_BLUE = rl.Color(112, 192, 216, 255) CEM_BLUE = PERSONALITY_BLUE TRAFFIC_RED = rl.Color(200, 32, 48, 255) def _with_alpha(color: rl.Color, alpha: int) -> rl.Color: return rl.Color(color.r, color.g, color.b, max(0, min(255, alpha))) def _env_truthy(name: str) -> bool: return os.getenv(name, "").lower() in {"1", "true", "yes", "on"} def _v(x: float, y: float) -> rl.Vector2: return rl.Vector2(float(x), float(y)) def _draw_line(x1: float, y1: float, x2: float, y2: float, thickness: float, color: rl.Color) -> None: rl.draw_line_ex(_v(x1, y1), _v(x2, y2), thickness, color) def _draw_tri(p1: tuple[float, float], p2: tuple[float, float], p3: tuple[float, float], color: rl.Color) -> None: rl.draw_triangle(_v(*p1), _v(*p2), _v(*p3), color) def _draw_quad(points: tuple[tuple[float, float], tuple[float, float], tuple[float, float], tuple[float, float]], color: rl.Color) -> None: p1, p2, p3, p4 = points rl.draw_triangle(_v(*p1), _v(*p2), _v(*p3), color) rl.draw_triangle(_v(*p1), _v(*p3), _v(*p4), color) def _draw_quad_outline(points: tuple[tuple[float, float], tuple[float, float], tuple[float, float], tuple[float, float]], thickness: float, color: rl.Color) -> None: for idx, start in enumerate(points): end = points[(idx + 1) % len(points)] _draw_line(start[0], start[1], end[0], end[1], thickness, color) def _draw_bezier(p0: tuple[float, float], p1: tuple[float, float], p2: tuple[float, float], p3: tuple[float, float], thickness: float, color: rl.Color) -> None: rl.draw_spline_segment_bezier_cubic(_v(*p0), _v(*p1), _v(*p2), _v(*p3), thickness, color) def _bezier_point(p0: tuple[float, float], p1: tuple[float, float], p2: tuple[float, float], p3: tuple[float, float], t: float) -> tuple[float, float]: inv_t = 1.0 - t x = inv_t ** 3 * p0[0] + 3 * inv_t ** 2 * t * p1[0] + 3 * inv_t * t ** 2 * p2[0] + t ** 3 * p3[0] y = inv_t ** 3 * p0[1] + 3 * inv_t ** 2 * t * p1[1] + 3 * inv_t * t ** 2 * p2[1] + t ** 3 * p3[1] return x, y def _draw_dashed_bezier(p0: tuple[float, float], p1: tuple[float, float], p2: tuple[float, float], p3: tuple[float, float], thickness: float, color: rl.Color) -> None: for start_t, end_t in ((0.18, 0.28), (0.39, 0.50), (0.61, 0.72)): start = _bezier_point(p0, p1, p2, p3, start_t) end = _bezier_point(p0, p1, p2, p3, end_t) _draw_line(start[0], start[1], end[0], end[1], thickness, color) class MiciSidebarWidgets(Widget): def __init__(self, confidence_ball: ConfidenceBall): super().__init__() self._confidence_ball = confidence_ball self._font_bold = gui_app.font(FontWeight.BOLD) self._font_semi_bold = gui_app.font(FontWeight.SEMI_BOLD) self._demo = _env_truthy("SP_CEM_DEMO") or _env_truthy("SP_MICI_WIDGET_DEMO") @property def demo_active(self) -> bool: return self._demo def _render(self, rect: rl.Rectangle) -> None: sidebar = rl.Rectangle( rect.x + rect.width - SIDE_PANEL_WIDTH, rect.y, SIDE_PANEL_WIDTH, rect.height, ) rl.draw_rectangle(int(sidebar.x), int(sidebar.y), int(sidebar.width), int(sidebar.height), BLACK) slot_height = sidebar.height / 3 confidence_slot = rl.Rectangle(sidebar.x, sidebar.y, sidebar.width, slot_height) cem_slot = rl.Rectangle(sidebar.x, sidebar.y + slot_height, sidebar.width, slot_height) personality_slot = rl.Rectangle(sidebar.x, sidebar.y + slot_height * 2, sidebar.width, sidebar.height - slot_height * 2) self._confidence_ball.render_static(confidence_slot, max(16, min(20, int(slot_height * 0.28)))) self._draw_cem_widget(cem_slot) self._draw_personality_widget(personality_slot) def _draw_personality_widget(self, rect: rl.Rectangle) -> None: center_x = rect.x + rect.width / 2 center_y = rect.y + rect.height / 2 icon_height = min(rect.height - 6, 64) bottom_y = center_y + icon_height * 0.42 lane_top_y = center_y - icon_height * 0.38 traffic_mode, personality = self._personality_state() active_color = TRAFFIC_RED if traffic_mode else PERSONALITY_BLUE stack_count = 1 if traffic_mode else max(1, min(3, personality + 1)) _draw_line(center_x - 23, bottom_y, center_x - 14, lane_top_y, 3, WHITE) _draw_line(center_x + 23, bottom_y, center_x + 14, lane_top_y, 3, WHITE) car_top_y = bottom_y - 4 car_bottom_y = bottom_y + 8 car_outline = ( (center_x - 8, car_top_y), (center_x + 8, car_top_y), (center_x + 13, car_bottom_y), (center_x - 13, car_bottom_y), ) _draw_quad_outline(car_outline, 3, WHITE) bar_h = max(6, min(9, int(icon_height * 0.14))) gap = max(3, min(5, int(icon_height * 0.08))) y = bottom_y - bar_h - 4 widths = ((36, 30), (28, 23), (20, 16)) for idx in range(stack_count): bottom_w, top_w = widths[idx] top_y = y bottom = y + bar_h points = ( (center_x - top_w / 2, top_y), (center_x + top_w / 2, top_y), (center_x + bottom_w / 2, bottom), (center_x - bottom_w / 2, bottom), ) _draw_quad(points, _with_alpha(active_color, 60)) _draw_quad_outline(points, 3, active_color) y -= bar_h + gap def _personality_state(self) -> tuple[bool, int]: if self._demo: return DEMO_PERSONALITIES[int(rl.get_time() / DEMO_HOLD_SECONDS) % len(DEMO_PERSONALITIES)] personality = 1 try: if ui_state.sm.valid.get("selfdriveState", False): personality = PERSONALITY_TO_INT[ui_state.sm["selfdriveState"].personality] elif isinstance(ui_state.personality, int): personality = ui_state.personality else: personality = PERSONALITY_TO_INT[ui_state.personality] except Exception: personality = 1 return ui_state.traffic_mode_enabled, personality def _draw_cem_widget(self, rect: rl.Rectangle) -> None: reason, color = self._cem_reason() if reason == "chill": self._draw_chill_icon(rect) elif reason == "lead": self._draw_lead_icon(rect, color) elif reason == "stop": self._draw_stop_light_icon(rect) elif reason == "curve": self._draw_curve_icon(rect, color) elif reason == "turn": self._draw_turn_icon(rect, color) elif reason == "speed": self._draw_speed_icon(rect, color) else: self._draw_chill_icon(rect) def _model_stop_active(self) -> bool: try: if ui_state.sm.recv_frame["starpilotPlan"] >= ui_state.started_frame: starpilot_plan = ui_state.sm["starpilotPlan"] return bool(starpilot_plan.redLight or starpilot_plan.forcingStop) except Exception: pass return False def _curve_speed_controller_active(self) -> bool: try: if ui_state.sm.recv_frame["starpilotPlan"] >= ui_state.started_frame: return bool(ui_state.sm["starpilotPlan"].cscControllingSpeed) except Exception: pass return False def _cem_reason(self) -> tuple[str, rl.Color]: if self._demo: status = ui_state.params_memory.get_int("CEStatus", default=CEStatus["OFF"]) status_reason = self._ce_status_reason(status) if status_reason is not None: return status_reason return self._fallback_demo_reason() if self._model_stop_active(): return "stop", TRAFFIC_RED if self._curve_speed_controller_active(): return "curve", CEM_BLUE conditional_experimental = ui_state.params.get_bool("ConditionalExperimental") conditional_chill = ui_state.params.get_bool("ConditionalChill") and not conditional_experimental if conditional_chill: status = ui_state.params_memory.get_int("CCStatus", default=CCStatus["OFF"]) if status == CCStatus["LEAD"]: return "lead", CEM_BLUE if status == CCStatus["SPEED"]: return "speed", CEM_BLUE if status == CCStatus["USER_EXPERIMENTAL"]: return "chill", WHITE if status == CCStatus["USER_CHILL"]: return "chill", WHITE return "chill", WHITE status = ui_state.params_memory.get_int("CEStatus", default=CEStatus["OFF"]) if conditional_experimental else CEStatus["OFF"] status_reason = self._ce_status_reason(status) if status_reason is not None: return status_reason return "chill", WHITE def _fallback_demo_reason(self) -> tuple[str, rl.Color]: reason = DEMO_REASONS[int(rl.get_time() / DEMO_HOLD_SECONDS) % len(DEMO_REASONS)] if reason == "chill": return reason, WHITE if reason == "stop": return reason, TRAFFIC_RED return reason, CEM_BLUE def _ce_status_reason(self, status: int) -> tuple[str, rl.Color] | None: if status == CEStatus["CURVATURE"]: return "curve", CEM_BLUE if status == CEStatus["LEAD"]: return "lead", CEM_BLUE if status == CEStatus["SIGNAL"]: return "turn", CEM_BLUE if status in (CEStatus["SPEED"], CEStatus["SPEED_LIMIT"]): return "speed", CEM_BLUE if status == CEStatus["STOP_LIGHT"]: return "stop", TRAFFIC_RED if status == CEStatus["USER_DISABLED"]: return "chill", WHITE if status == CEStatus["USER_OVERRIDDEN"]: return "chill", WHITE return None def _draw_chill_icon(self, rect: rl.Rectangle) -> None: cx = rect.x + rect.width / 2 cy = rect.y + rect.height / 2 stroke = 3 _draw_line(cx - 14, cy - 15, cx + 14, cy - 15, stroke, WHITE) _draw_line(cx - 16, cy - 13, cx - 16, cy + 1, stroke, WHITE) _draw_line(cx + 16, cy - 13, cx + 16, cy + 1, stroke, WHITE) _draw_line(cx - 19, cy + 3, cx + 19, cy + 3, stroke, WHITE) _draw_line(cx - 21, cy - 3, cx - 21, cy + 10, stroke, WHITE) _draw_line(cx + 21, cy - 3, cx + 21, cy + 10, stroke, WHITE) _draw_line(cx - 21, cy + 10, cx + 21, cy + 10, stroke, WHITE) _draw_line(cx - 14, cy + 6, cx + 14, cy + 6, 2, CEM_BLUE) _draw_line(cx - 14, cy + 12, cx - 17, cy + 19, stroke, WHITE) _draw_line(cx + 14, cy + 12, cx + 17, cy + 19, stroke, WHITE) def _draw_lead_icon(self, rect: rl.Rectangle, color: rl.Color) -> None: cx = rect.x + rect.width / 2 cy = rect.y + rect.height / 2 self._draw_car(cx, cy - 13, 31, 18, WHITE, TRAFFIC_RED) self._draw_car(cx, cy + 12, 43, 22, WHITE, TRAFFIC_RED) _draw_line(cx - 11, cy - 1, cx - 11, cy + 3, 2, _with_alpha(WHITE, 130)) _draw_line(cx + 11, cy - 1, cx + 11, cy + 3, 2, _with_alpha(WHITE, 130)) def _draw_car(self, cx: float, cy: float, width: float, height: float, color: rl.Color, accent: rl.Color) -> None: top = cy - height / 2 bottom = cy + height / 2 body = ( (cx - width * 0.34, top + height * 0.10), (cx + width * 0.34, top + height * 0.10), (cx + width * 0.50, bottom - height * 0.04), (cx - width * 0.50, bottom - height * 0.04), ) windshield = ( (cx - width * 0.22, top + height * 0.24), (cx + width * 0.22, top + height * 0.24), (cx + width * 0.32, top + height * 0.50), (cx - width * 0.32, top + height * 0.50), ) _draw_quad_outline(body, 3, color) _draw_quad_outline(windshield, 2, color) _draw_line(cx - width * 0.56, top + height * 0.46, cx - width * 0.50, top + height * 0.46, 2, color) _draw_line(cx + width * 0.50, top + height * 0.46, cx + width * 0.56, top + height * 0.46, 2, color) _draw_line(cx - width * 0.32, bottom - height * 0.22, cx - width * 0.16, bottom - height * 0.22, 2, accent) _draw_line(cx + width * 0.16, bottom - height * 0.22, cx + width * 0.32, bottom - height * 0.22, 2, accent) def _draw_stop_light_icon(self, rect: rl.Rectangle) -> None: cx = rect.x + rect.width / 2 cy = rect.y + rect.height / 2 housing = rl.Rectangle(cx - 12, cy - 27, 24, 54) rl.draw_rectangle_rounded_lines_ex(housing, 0.35, 8, 3, WHITE) for bulb_y in (cy - 16, cy, cy + 16): rl.draw_circle_lines(int(cx), int(bulb_y), 6, WHITE) rl.draw_circle_lines(int(cx), int(cy - 16), 7, TRAFFIC_RED) rl.draw_circle(int(cx), int(cy - 16), 4, TRAFFIC_RED) def _draw_curve_icon(self, rect: rl.Rectangle, color: rl.Color) -> None: cx = rect.x + rect.width / 2 cy = rect.y + rect.height / 2 def road_curve_points(x_offset: float) -> tuple[tuple[float, float], tuple[float, float], tuple[float, float], tuple[float, float]]: return ( (cx + x_offset, cy + 24), (cx + x_offset - 1, cy + 9), (cx + x_offset + 1, cy - 9), (cx + x_offset + 6, cy - 24), ) for points in (road_curve_points(-18), road_curve_points(14)): _draw_bezier(*points, 3, WHITE) _draw_dashed_bezier(*road_curve_points(-2), 4, color) def _draw_turn_icon(self, rect: rl.Rectangle, color: rl.Color) -> None: cx = rect.x + rect.width / 2 cy = rect.y + rect.height / 2 left = False right = True try: left = ui_state.sm["carState"].leftBlinker right = ui_state.sm["carState"].rightBlinker except Exception: pass direction = -1 if left or not right else 1 shaft = ( (cx - direction * 10, cy + 25), (cx - direction * 13, cy + 6), (cx - direction * 4, cy - 8), (cx + direction * 18, cy - 20), ) _draw_bezier(*shaft, 4, color) tip = (cx + direction * 24, cy - 26) _draw_tri( tip, (cx + direction * 4, cy - 24), (cx + direction * 19, cy - 7), color, ) def _draw_speed_icon(self, rect: rl.Rectangle, color: rl.Color) -> None: cx = rect.x + rect.width / 2 cy = rect.y + rect.height / 2 radius = min(rect.width, rect.height) * 0.34 text = self._speed_text() rl.draw_circle(int(cx), int(cy), radius - 2.5, WHITE) rl.draw_ring(_v(cx, cy), radius - 5, radius, 0, 360, 48, color) font_size = 19 if len(text) <= 2 else 16 text_size = measure_text_cached(self._font_bold, text, font_size) rl.draw_text_ex(self._font_bold, text, rl.Vector2(cx - text_size.x / 2, cy - text_size.y / 2 + 1), font_size, 0, BLACK) def _speed_text(self) -> str: if ui_state.sm.recv_frame["starpilotPlan"] < ui_state.started_frame: return "S" speed_limit = ui_state.sm["starpilotPlan"].slcSpeedLimit if speed_limit <= 0: return "S" conversion = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH speed_text = str(round(speed_limit * conversion)) return speed_text[:3]