Files
IQ.Pilot/iqpilot/selfdrive/ui/onroad/hud_renderer.py
2026-09-03 18:23:24 -05:00

252 lines
8.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import pyray as rl
from dataclasses import dataclass
from iqpilot.common.constants import CV
from iqpilot.selfdrive.ui.onroad.exp_button import ExpButton
from iqpilot.selfdrive.ui.ui_state import ui_state, UIStatus
from iqpilot.system.ui.lib.application import gui_app, FontWeight
from iqpilot.system.ui.lib.multilang import tr
from iqpilot.system.ui.lib.text_measure import measure_text_cached
from iqpilot.system.ui.widgets import Widget
# Constants
SET_SPEED_NA = 255
KM_TO_MILE = 0.621371
CRUISE_DISABLED_CHAR = ''
@dataclass(frozen=True)
class UIConfig:
header_height: int = 300
border_size: int = 30
button_size: int = 192
set_speed_width_metric: int = 186
set_speed_width_imperial: int = 174
set_speed_height: int = 228
wheel_icon_size: int = 144
@dataclass(frozen=True)
class FontSizes:
current_speed: int = 176
speed_unit: int = 66
max_speed: int = 28
set_speed: int = 74
limit_speed: int = 64
limit_offset: int = 30
limit_unit: int = 22
limit_label: int = 24
@dataclass(frozen=True)
class Colors:
WHITE = rl.WHITE
DISENGAGED = rl.Color(145, 155, 149, 255)
OVERRIDE = rl.Color(145, 155, 149, 255) # Added
ENGAGED = rl.Color(0x0C, 0x94, 0x96, 0xFF)
LIMIT_ENGAGED = rl.Color(0x27, 0xF5, 0xD3, 0xFF)
DISENGAGED_BG = rl.Color(0, 0, 0, 153)
OVERRIDE_BG = rl.Color(145, 155, 149, 204)
ENGAGED_BG = rl.Color(128, 216, 166, 204)
GREY = rl.Color(166, 166, 166, 255)
DARK_GREY = rl.Color(114, 114, 114, 255)
BLACK_TRANSLUCENT = rl.Color(0, 0, 0, 166)
WHITE_TRANSLUCENT = rl.Color(255, 255, 255, 200)
BORDER_TRANSLUCENT = rl.Color(255, 255, 255, 75)
HEADER_GRADIENT_START = rl.Color(0, 0, 0, 114)
HEADER_GRADIENT_END = rl.BLANK
UI_CONFIG = UIConfig()
FONT_SIZES = FontSizes()
COLORS = Colors()
class HudRenderer(Widget):
def __init__(self):
super().__init__()
"""Initialize the HUD renderer."""
self.is_cruise_set: bool = False
self.is_cruise_available: bool = True
self.set_speed: float = SET_SPEED_NA
self.speed: float = 0.0
self.v_ego_cluster_seen: bool = False
self.limit_speed_text: str = "---"
self.limit_offset_text: str = ""
self.limit_available: bool = False
self._font_semi_bold: rl.Font = gui_app.font(FontWeight.SEMI_BOLD)
self._font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
self._font_medium: rl.Font = gui_app.font(FontWeight.MEDIUM)
self._exp_button: ExpButton = ExpButton(UI_CONFIG.button_size, UI_CONFIG.wheel_icon_size)
def _update_state(self) -> None:
"""Update HUD state based on car state and controls state."""
sm = ui_state.sm
if sm.recv_frame["carState"] < ui_state.started_frame:
self.is_cruise_set = False
self.set_speed = SET_SPEED_NA
self.speed = 0.0
return
controls_state = sm['controlsState']
car_state = sm['carState']
v_cruise_cluster = car_state.vCruiseCluster
self.set_speed = (
controls_state.vCruiseDEPRECATED if v_cruise_cluster == 0.0 else v_cruise_cluster
)
self.is_cruise_set = 0 < self.set_speed < SET_SPEED_NA
self.is_cruise_available = self.set_speed != -1
if self.is_cruise_set and not ui_state.is_metric:
self.set_speed *= KM_TO_MILE
v_ego_cluster = car_state.vEgoCluster
self.v_ego_cluster_seen = self.v_ego_cluster_seen or v_ego_cluster != 0.0
v_ego = v_ego_cluster if self.v_ego_cluster_seen else car_state.vEgo
speed_conversion = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
self.speed = max(0.0, v_ego * speed_conversion)
def _render(self, rect: rl.Rectangle) -> None:
"""Render HUD elements to the screen."""
# Draw the header background
rl.draw_rectangle_gradient_v(
int(rect.x),
int(rect.y),
int(rect.width),
UI_CONFIG.header_height,
COLORS.HEADER_GRADIENT_START,
COLORS.HEADER_GRADIENT_END,
)
if self.is_cruise_available:
self._draw_set_speed(rect)
self._draw_current_speed(rect)
button_x = rect.x + rect.width - UI_CONFIG.border_size - UI_CONFIG.button_size
button_y = rect.y + UI_CONFIG.border_size
self._exp_button.render(rl.Rectangle(button_x, button_y, UI_CONFIG.button_size, UI_CONFIG.button_size))
def user_interacting(self) -> bool:
return self._exp_button.is_pressed
def _draw_set_speed(self, rect: rl.Rectangle) -> None:
"""Draw the compact stacked LIMIT / MAX speed indicator box."""
set_speed_width = UI_CONFIG.set_speed_width_metric if ui_state.is_metric else UI_CONFIG.set_speed_width_imperial
x = rect.x + 60 + (UI_CONFIG.set_speed_width_imperial - set_speed_width) // 2
y = rect.y + 45
set_speed_rect = rl.Rectangle(x, y, set_speed_width, UI_CONFIG.set_speed_height)
rl.draw_rectangle_rounded(set_speed_rect, 0.35, 10, COLORS.BLACK_TRANSLUCENT)
rl.draw_rectangle_rounded_lines_ex(set_speed_rect, 0.35, 10, 6, COLORS.BORDER_TRANSLUCENT)
split_y = y + 112
rl.draw_line_ex(
rl.Vector2(x + 18, split_y),
rl.Vector2(x + set_speed_width - 18, split_y),
3,
COLORS.BORDER_TRANSLUCENT,
)
max_color = COLORS.DARK_GREY
set_speed_color = COLORS.WHITE
limit_value_color = COLORS.WHITE if self.limit_available else COLORS.DARK_GREY
if self.is_cruise_set:
if ui_state.status == UIStatus.ENGAGED:
max_color = COLORS.ENGAGED
if self.limit_available:
limit_value_color = COLORS.LIMIT_ENGAGED
elif ui_state.status == UIStatus.DISENGAGED:
max_color = COLORS.DISENGAGED
elif ui_state.status == UIStatus.OVERRIDE:
max_color = COLORS.OVERRIDE
limit_value_text = self.limit_speed_text
if len(limit_value_text) <= 2:
limit_value_size = FONT_SIZES.limit_speed
elif len(limit_value_text) == 3:
limit_value_size = 56
else:
limit_value_size = 48
limit_value_width = measure_text_cached(self._font_bold, limit_value_text, limit_value_size).x
limit_offset_text = self.limit_offset_text if self.limit_available else ""
limit_offset_width = 0.0
if limit_offset_text:
limit_offset_width = measure_text_cached(self._font_semi_bold, limit_offset_text, FONT_SIZES.limit_offset).x + 8
limit_value_x = x + (set_speed_width - limit_value_width - limit_offset_width) / 2
rl.draw_text_ex(
self._font_bold,
limit_value_text,
rl.Vector2(limit_value_x, y + 14),
limit_value_size,
0,
limit_value_color,
)
if limit_offset_text:
rl.draw_text_ex(
self._font_semi_bold,
limit_offset_text,
rl.Vector2(limit_value_x + limit_value_width + 8, y + 22),
FONT_SIZES.limit_offset,
0,
COLORS.WHITE_TRANSLUCENT,
)
if self.limit_available:
limit_unit_text = tr("LIMIT")
limit_unit_width = measure_text_cached(self._font_medium, limit_unit_text, FONT_SIZES.limit_unit).x
rl.draw_text_ex(
self._font_medium,
limit_unit_text,
rl.Vector2(x + (set_speed_width - limit_unit_width) / 2, y + 78),
FONT_SIZES.limit_unit,
0,
COLORS.WHITE_TRANSLUCENT,
)
else:
limit_label_text = tr("LIMIT")
limit_label_width = measure_text_cached(self._font_semi_bold, limit_label_text, FONT_SIZES.limit_label).x
rl.draw_text_ex(
self._font_semi_bold,
limit_label_text,
rl.Vector2(x + (set_speed_width - limit_label_width) / 2, y + 74),
FONT_SIZES.limit_label,
0,
COLORS.GREY,
)
max_text = tr("MAX")
max_text_width = measure_text_cached(self._font_semi_bold, max_text, FONT_SIZES.max_speed).x
rl.draw_text_ex(
self._font_semi_bold,
max_text,
rl.Vector2(x + (set_speed_width - max_text_width) / 2, y + 118),
FONT_SIZES.max_speed,
0,
max_color,
)
set_speed_text = CRUISE_DISABLED_CHAR if not self.is_cruise_set else str(round(self.set_speed))
speed_text_width = measure_text_cached(self._font_bold, set_speed_text, FONT_SIZES.set_speed).x
rl.draw_text_ex(
self._font_bold,
set_speed_text,
rl.Vector2(x + (set_speed_width - speed_text_width) / 2, y + 136),
FONT_SIZES.set_speed,
0,
set_speed_color,
)
def _draw_current_speed(self, rect: rl.Rectangle) -> None:
"""Draw the current vehicle speed and unit."""
speed_text = str(round(self.speed))
speed_text_size = measure_text_cached(self._font_bold, speed_text, FONT_SIZES.current_speed)
speed_pos = rl.Vector2(rect.x + rect.width / 2 - speed_text_size.x / 2, 180 - speed_text_size.y / 2)
rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, COLORS.WHITE)
unit_text = tr("km/h") if ui_state.is_metric else tr("mph")
unit_text_size = measure_text_cached(self._font_medium, unit_text, FONT_SIZES.speed_unit)
unit_pos = rl.Vector2(rect.x + rect.width / 2 - unit_text_size.x / 2, 290 - unit_text_size.y / 2)
rl.draw_text_ex(self._font_medium, unit_text, unit_pos, FONT_SIZES.speed_unit, 0, COLORS.WHITE_TRANSLUCENT)