You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
178 lines
6.8 KiB
178 lines
6.8 KiB
import pyray as rl
|
|
from dataclasses import dataclass
|
|
from cereal.messaging import SubMaster
|
|
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
|
from openpilot.common.conversions import Conversions as CV
|
|
|
|
# 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 = 200
|
|
set_speed_width_imperial: int = 172
|
|
set_speed_height: int = 204
|
|
wheel_icon_size: int = 144
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class FontSizes:
|
|
current_speed: int = 176
|
|
speed_unit: int = 66
|
|
max_speed: int = 40
|
|
set_speed: int = 90
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Colors:
|
|
white: rl.Color = rl.Color(255, 255, 255, 255)
|
|
disengaged: rl.Color = rl.Color(145, 155, 149, 255)
|
|
override: rl.Color = rl.Color(145, 155, 149, 255) # Added
|
|
engaged: rl.Color = rl.Color(128, 216, 166, 255)
|
|
disengaged_bg: rl.Color = rl.Color(0, 0, 0, 153)
|
|
override_bg: rl.Color = rl.Color(145, 155, 149, 204)
|
|
engaged_bg: rl.Color = rl.Color(128, 216, 166, 204)
|
|
grey: rl.Color = rl.Color(166, 166, 166, 255)
|
|
dark_grey: rl.Color = rl.Color(114, 114, 114, 255)
|
|
black_translucent: rl.Color = rl.Color(0, 0, 0, 166)
|
|
white_translucent: rl.Color = rl.Color(255, 255, 255, 200)
|
|
border_translucent: rl.Color = rl.Color(255, 255, 255, 75)
|
|
header_gradient_start: rl.Color = rl.Color(0, 0, 0, 114)
|
|
header_gradient_end: rl.Color = rl.Color(0, 0, 0, 0)
|
|
|
|
|
|
UI_CONFIG = UIConfig()
|
|
FONT_SIZES = FontSizes()
|
|
COLORS = Colors()
|
|
|
|
|
|
class HudRenderer:
|
|
def __init__(self):
|
|
"""Initialize the HUD renderer."""
|
|
self.is_cruise_set: bool = False
|
|
self.is_cruise_available: bool = False
|
|
self.set_speed: float = SET_SPEED_NA
|
|
self.speed: float = 0.0
|
|
self.v_ego_cluster_seen: bool = False
|
|
self._wheel_texture: rl.Texture = gui_app.texture('icons/chffr_wheel.png', UI_CONFIG.wheel_icon_size, UI_CONFIG.wheel_icon_size)
|
|
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)
|
|
|
|
def _update_state(self, sm: SubMaster) -> None:
|
|
"""Update HUD state based on car state and controls state."""
|
|
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 draw(self, rect: rl.Rectangle, sm: SubMaster) -> None:
|
|
"""Render HUD elements to the screen."""
|
|
self._update_state(sm)
|
|
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)
|
|
self._draw_wheel_icon(rect)
|
|
|
|
def _draw_set_speed(self, rect: rl.Rectangle) -> None:
|
|
"""Draw the 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.2, 30, COLORS.black_translucent)
|
|
rl.draw_rectangle_rounded_lines_ex(set_speed_rect, 0.2, 30, 6, COLORS.border_translucent)
|
|
|
|
max_color = COLORS.grey
|
|
set_speed_color = COLORS.dark_grey
|
|
if self.is_cruise_set:
|
|
set_speed_color = COLORS.white
|
|
if ui_state.status == UIStatus.ENGAGED:
|
|
max_color = COLORS.engaged
|
|
elif ui_state.status == UIStatus.DISENGAGED:
|
|
max_color = COLORS.disengaged
|
|
elif ui_state.status == UIStatus.OVERRIDE:
|
|
max_color = COLORS.override
|
|
|
|
max_text = "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 + 27),
|
|
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 + 77),
|
|
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 = "km/h" if ui_state.is_metric else "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)
|
|
|
|
def _draw_wheel_icon(self, rect: rl.Rectangle) -> None:
|
|
"""Draw the steering wheel icon with status-based opacity."""
|
|
center_x = int(rect.x + rect.width - UI_CONFIG.border_size - UI_CONFIG.button_size / 2)
|
|
center_y = int(rect.y + UI_CONFIG.border_size + UI_CONFIG.button_size / 2)
|
|
rl.draw_circle(center_x, center_y, UI_CONFIG.button_size / 2, COLORS.black_translucent)
|
|
|
|
opacity = 0.7 if ui_state.status == UIStatus.DISENGAGED else 1.0
|
|
img_pos = rl.Vector2(center_x - self._wheel_texture.width / 2, center_y - self._wheel_texture.height / 2)
|
|
rl.draw_texture_v(self._wheel_texture, img_pos, rl.Color(255, 255, 255, int(255 * opacity)))
|
|
|