Files
carrotpilot/selfdrive/ui/onroad/hud_renderer.py
2026-04-18 12:45:41 +09:00

1409 lines
41 KiB
Python
Raw 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 time
from collections import deque
import pyray as rl
from dataclasses import dataclass
from openpilot.common.constants import CV
from openpilot.selfdrive.ui.onroad.exp_button import ExpButton
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.multilang import tr
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.text_draw import draw_text_ui_style
from openpilot.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 = 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.WHITE
DISENGAGED = rl.Color(145, 155, 149, 255)
OVERRIDE = rl.Color(145, 155, 149, 255) # Added
ENGAGED = rl.Color(128, 216, 166, 255)
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()
@dataclass(frozen=True)
class SetSpeedOverrideState:
active: bool
speed_kph: float
label: str
speed_color_mode: int # 0: white, 1: green, 2: orange
force_persist: bool
class SetSpeedOverride:
def compute(self, sm, set_speed_kph: float) -> SetSpeedOverrideState:
# 1) eco (highest)
cruise_target = None
try:
cruise_target = float(sm['longitudinalPlan'].cruiseTarget)
except Exception:
cruise_target = None
if cruise_target is not None and cruise_target > (set_speed_kph + 0.5):
return SetSpeedOverrideState(
active=True,
speed_kph=cruise_target,
label="eco",
speed_color_mode=1,
force_persist=True, # eco 조건 유지되는 동안 계속 표시
)
# 2) apply_speed (desiredSpeed/source)
desired_speed = None
desired_source = ""
try:
desired_speed = float(sm['carrotMan'].desiredSpeed)
desired_source = str(sm['carrotMan'].desiredSource or "")
except Exception:
desired_speed = None
desired_source = ""
if desired_speed is not None and 0 < desired_speed < 200 and desired_speed < set_speed_kph:
label = desired_source.strip() or "apply"
label = label[:8] # 너무 길면 UI 깨짐 방지 (원하면 길이 조절)
return SetSpeedOverrideState(
active=True,
speed_kph=desired_speed,
label=label,
speed_color_mode=2,
force_persist=True, # 조건 유지되는 동안 계속 표시
)
# 3) default
return SetSpeedOverrideState(
active=False,
speed_kph=set_speed_kph,
label=tr("MAX"),
speed_color_mode=0,
force_persist=False,
)
class HudRenderer(Widget):
def __init__(self):
super().__init__()
self.is_cruise_set = False
self.is_cruise_available = True
self.set_speed = SET_SPEED_NA
self.speed = 0.0
self.v_ego_cluster_seen = False
self._font_semi_bold = gui_app.font(FontWeight.SEMI_BOLD)
self._font_bold = gui_app.font(FontWeight.BOLD)
self._font_medium = gui_app.font(FontWeight.MEDIUM)
self._font_display = gui_app.font(FontWeight.DISPLAY)
self._exp_button = ExpButton(UI_CONFIG.button_size, UI_CONFIG.wheel_icon_size)
self._txt_speed_bg = gui_app.texture('images/speed_bg.png')
# traffic light icon들 이름은 실제 프로젝트 리소스 이름에 맞춰 수정 가능
self._traffic_red_icon = gui_app.texture('images/traffic_red.png')
self._traffic_green_icon = gui_app.texture('images/traffic_green.png')
self._ic_turn_l = gui_app.texture('images/turn_l.png')
self._ic_turn_r = gui_app.texture('images/turn_r.png')
self._ic_lane_change_l = gui_app.texture('images/lane_change_l.png')
self._ic_lane_change_r = gui_app.texture('images/lane_change_r.png')
self._ic_turn_u = gui_app.texture('images/turn_u.png')
self._set_speed_override = SetSpeedOverride()
self._debug_speed_panel = False
self._engaged = False
self._blink_timer = 0
self._disp_timer = 0
self._cpu_temp = 0.0
self._cpu_usage = 0.0
self._memory_usage = 0
self._free_space = 0.0
self._voltage = 0.0
self._plot_renderer = None
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.deprecated.vCruise 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
self._engaged = sm['selfdriveState'].enabled
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_carrot(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))
if self._plot_renderer is None:
self._plot_renderer = PlotRenderer()
self._plot_renderer.draw(rect, self._font_display)
self._draw_date_time(rect)
self._draw_tpms_top_right(rect)
def user_interacting(self) -> bool:
return self._exp_button.is_pressed
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.35, 10, COLORS.BLACK_TRANSLUCENT)
rl.draw_rectangle_rounded_lines_ex(set_speed_rect, 0.35, 10, 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 = 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 + 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 = 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)
def _draw_round_box(self, x, y, w, h, fill_color,
line_color=None,
roundness=0.25,
segments=8,
line_thickness=2):
rect = rl.Rectangle(float(x), float(y), float(w), float(h))
rl.draw_rectangle_rounded(rect, roundness, segments, fill_color)
if line_color is not None and line_thickness > 0:
rl.draw_rectangle_rounded_lines_ex(rect, roundness, segments, float(line_thickness), line_color)
def _draw_texture_rect(self, tex, x, y, w, h, tint=rl.WHITE):
if tex is None:
return
rl.draw_texture_pro(
tex,
rl.Rectangle(0, 0, float(tex.width), float(tex.height)),
rl.Rectangle(float(x), float(y), float(w), float(h)),
rl.Vector2(0, 0),
0.0,
tint,
)
def _get_gear_text(self) -> str:
sm = ui_state.sm
try:
car_state = sm["carState"]
gear = car_state.gearShifter
except Exception:
return "R"
# cereal enum → 문자열 변환
try:
gear_name = str(gear).split('.')[-1]
except Exception:
gear_name = str(gear)
# DRIVE 처리
if "DRIVE" in gear_name.upper():
try:
step = int(car_state.gearStep)
if step > 0:
return str(step)
else:
return "D"
except Exception:
return "D"
if "PARK" in gear_name.upper():
return "P"
if "REVERSE" in gear_name.upper():
return "R"
if "NEUTRAL" in gear_name.upper():
return "N"
if "SPORT" in gear_name.upper():
return "S"
if "LOW" in gear_name.upper():
return "L"
if "BRAKE" in gear_name.upper():
return "B"
if "ECO" in gear_name.upper():
return "E"
if "UNKNOWN" in gear_name.upper():
return "U"
return "M"
def _get_cruise_gap(self) -> int:
try:
personality = ui_state.params.get_int("LongitudinalPersonality")
gap = int(personality) + 1
except Exception:
gap = 8
return gap
def _get_driving_mode_text_and_color(self) -> tuple[str, rl.Color]:
try:
mode_val = int(ui_state.sm["longitudinalPlan"].myDrivingMode)
except Exception:
return "", rl.Color(255, 255, 255, 200)
if mode_val == 1: # eco
return tr("eco"), rl.Color(0, 255, 0, 200)
if mode_val == 2: # safe
return tr("safe"), rl.Color(255, 165, 0, 200)
if mode_val == 3: # normal
return tr("norm"), rl.Color(255, 255, 255, 200)
if mode_val == 4: # high
return tr("high"), rl.Color(255, 0, 0, 200)
return "", rl.Color(255, 255, 255, 200)
def _update_device_info(self):
sm = ui_state.sm
self._cpu_temp = 0.0
self._cpu_usage = 0.0
self._memory_usage = 0
self._free_space = 0.0
self._voltage = 0.0
#self._plot_renderer = None
try:
device_state = sm["deviceState"]
self._free_space = float(device_state.freeSpacePercent)
self._memory_usage = int(device_state.memoryUsagePercent)
try:
cpu_temps = list(device_state.cpuTempC)
if len(cpu_temps) > 0:
self._cpu_temp = sum(cpu_temps) / len(cpu_temps)
except Exception:
pass
try:
cpu_usages = [float(v) for v in device_state.cpuUsagePercent if float(v) > 0]
if len(cpu_usages) > 0:
self._cpu_usage = sum(cpu_usages) / len(cpu_usages)
except Exception:
pass
except Exception:
pass
try:
peripheral_state = sm["peripheralState"]
self._voltage = float(peripheral_state.voltage) / 1000.0
except Exception:
pass
def _get_active_carrot(self) -> int:
try:
return int(ui_state.sm["carrotMan"].activeCarrot)
except Exception:
return 0
def _get_nav_path_vertex_count(self) -> int:
try:
return int(ui_state.sm["carrotMan"].navPathVertexCount)
except Exception:
return 0
def _get_traffic_state(self) -> int:
try:
return int(ui_state.sm["carrotMan"].trafficState)
except Exception:
return 0
def _get_traffic_state_carrot(self) -> int:
try:
return int(ui_state.sm["carrotMan"].trafficStateCarrot)
except Exception:
return 0
def _get_speed_limit_info(self):
"""
return:
x_spd_limit, x_sign_type, road_limit_speed
"""
try:
cm = ui_state.sm["carrotMan"]
x_spd_limit = int(cm.xSpdLimit)
except Exception:
x_spd_limit = 0
try:
cm = ui_state.sm["carrotMan"]
x_sign_type = int(cm.xSignType)
except Exception:
x_sign_type = 0
try:
cm = ui_state.sm["carrotMan"]
road_limit_speed = int(cm.nRoadLimitSpeed)
except Exception:
road_limit_speed = 0
return x_spd_limit, x_sign_type, road_limit_speed
def _gps_has_fix(self) -> bool:
sm = ui_state.sm
try:
return bool(sm["gpsLocationExternal"].hasFix)
except Exception:
pass
try:
return bool(sm["gpsLocation"].hasFix)
except Exception:
pass
return False
def _draw_carrot_traffic_light(self, bx: int, by: int):
traffic_state = self._get_traffic_state()
traffic_state_carrot = self._get_traffic_state_carrot()
icon_size = 64
red_light = traffic_state == 1
green_light = traffic_state == 2
icon_red = icon_size
icon_green = icon_size
if traffic_state_carrot == 1:
red_light = True
icon_red = int(icon_red * 1.5)
elif traffic_state_carrot == 2:
green_light = True
icon_green = int(icon_green * 1.5)
x = bx
y = by + 270
if red_light:
self._draw_texture_rect(self._traffic_red_icon, x - icon_red / 2, y - icon_red / 2, icon_red, icon_red)
elif green_light:
self._draw_texture_rect(self._traffic_green_icon, x - icon_green / 2, y - icon_green / 2, icon_green, icon_green)
def _draw_carrot_speed_panel(self, bx: int, by: int):
sm = ui_state.sm
ov = self._set_speed_override.compute(sm, float(self.set_speed))
self._draw_texture_rect(self._txt_speed_bg, bx - 100, by - 60, 350, 150)
cur_speed_int = 123 if self._debug_speed_panel else int(round(self.speed))
cur_text = str(cur_speed_int)
draw_text_ui_style(
cur_text, bx, by + 50, 120, rl.WHITE,
font=self._font_display,
border_width=3.0,
shadow_offset=8.0,
align="center_bottom",
)
if self._engaged and self.is_cruise_set:
set_speed = float(self.set_speed)
if not ui_state.is_metric:
set_speed *= KM_TO_MILE
cruise_text = str(int(round(set_speed)))
else:
cruise_text = "--"
draw_text_ui_style(
cruise_text, bx + 170, by + 15 + 5, 60, rl.GREEN,
font=self._font_display,
border_width=1.0,
shadow_offset=5.0,
align="center_bottom",
)
if ov.active:
ov_speed = float(ov.speed_kph)
if not ui_state.is_metric:
ov_speed *= KM_TO_MILE
ov_text = str(int(round(ov_speed)))
ov_label = ov.label
if ov.speed_color_mode == 1:
ov_color = rl.GREEN
elif ov.speed_color_mode == 2:
ov_color = rl.Color(255, 165, 0, 230)
else:
ov_color = rl.GREEN
if self._debug_speed_panel:
ov_text = "111"
ov_label = "vturn"
draw_text_ui_style(
ov_text, bx + 250, by - 50 + 5, 50, ov_color,
font=self._font_display,
border_width=1.0,
shadow_offset=5.0,
align="center_bottom",
)
draw_text_ui_style(
ov_label, bx + 250, by - 100, 30, ov_color,
font=self._font_display,
border_width=1.0,
shadow_offset=5.0,
align="center_bottom",
)
def _draw_carrot_lower_status(self, bx: int, by: int):
mode_text, mode_color = self._get_driving_mode_text_and_color()
if self._debug_speed_panel:
mode_text = "safe"
mode_color = rl.Color(255, 165, 0, 230)
# driving mode
if mode_text:
dx = bx - 50
dy = by + 175
self._draw_round_box(
dx - 55, dy - 38, 110, 48,
mode_color,
line_color=rl.WHITE,
roundness=0.25,
segments=8,
line_thickness=2,
)
draw_text_ui_style(
mode_text, dx, dy - 2, 32, rl.WHITE,
font=self._font_display,
border_width=2.0,
shadow_offset=4.0,
align="center_bottom",
)
if self._gps_has_fix():
draw_text_ui_style(
"GPS", dx, dy - 45, 30, rl.GREEN,
font=self._font_display,
border_width=2.0,
shadow_offset=4.0,
align="center_bottom",
)
# gap number
gap = self._get_cruise_gap()
draw_text_ui_style(
str(gap), bx + 220, by + 77, 40, rl.WHITE,
font=self._font_display,
border_width=2.0,
shadow_offset=4.0,
align="center_bottom",
)
# gap bars
dx = bx + 270
dy = by + 185
ddy = 80.0 / 4.0
for i in range(max(0, min(gap, 4))):
self._draw_round_box(
dx,
dy - ddy * (i + 1) + 2,
70,
ddy - 2,
rl.Color(0, 255, 0, 210),
line_color=rl.WHITE,
roundness=0.12,
segments=4,
line_thickness=2,
)
# gear
gear = self._get_gear_text()
gx = bx + 305
gy = by + 60
self._draw_round_box(
gx - 35, gy - 70, 70, 80,
rl.Color(0, 255, 0, 210),
line_color=rl.WHITE,
roundness=0.20,
segments=8,
line_thickness=3,
)
draw_text_ui_style(
gear, gx, gy + 5, 70, rl.WHITE,
font=self._font_display,
border_width=2.0,
shadow_offset=4.0,
align="center_bottom",
)
# active carrot
active_carrot = self._get_active_carrot()
dx = bx + 200
dy = by + 175
if active_carrot >= 2:
self._draw_round_box(
dx - 55, dy - 38, 110, 48,
rl.GREEN,
line_color=rl.WHITE,
roundness=0.25,
segments=8,
line_thickness=2,
)
draw_text_ui_style(
"APN", dx, dy, 40, rl.WHITE,
font=self._font_display,
border_width=2.0,
shadow_offset=4.0,
align="center_bottom",
)
elif active_carrot >= 1:
self._draw_round_box(
dx - 55, dy - 38, 110, 48,
rl.Color(0, 120, 255, 210),
line_color=rl.WHITE,
roundness=0.25,
segments=8,
line_thickness=2,
)
draw_text_ui_style(
"APM", dx, dy, 40, rl.WHITE,
font=self._font_display,
border_width=2.0,
shadow_offset=4.0,
align="center_bottom",
)
if self._get_nav_path_vertex_count() > 1:
draw_text_ui_style(
"ROUTE", dx, dy - 45, 30, rl.WHITE,
font=self._font_display,
border_width=2.0,
shadow_offset=4.0,
align="center_bottom",
)
def _draw_carrot_speed_limit_box(self, bx: int, by: int):
x_spd_limit, x_sign_type, road_limit_speed = self._get_speed_limit_info()
dx = bx + 75
dy = by + 175
disp_speed = 0
limit_color = rl.Color(0, 255, 0, 210)
label = "LIMIT"
if x_spd_limit > 0 and x_sign_type != 22:
disp_speed = int(x_spd_limit if ui_state.is_metric else (x_spd_limit * KM_TO_MILE + 0.5))
label = "CAM"
if self._blink_timer <= 8:
limit_color = rl.Color(255, 0, 0, 210)
else:
limit_color = rl.Color(255, 255, 0, 210)
else:
disp_speed = int(road_limit_speed if ui_state.is_metric else (road_limit_speed * KM_TO_MILE + 0.5))
if self.speed > disp_speed + 2:
limit_color = rl.Color(255, 0, 0, 210)
else:
limit_color = rl.Color(255, 255, 255, 210)
draw_text_ui_style(
label, dx, dy - 45, 30, rl.WHITE,
font=self._font_display,
border_width=2.0,
shadow_offset=4.0,
align="center_bottom",
)
self._draw_round_box(
dx - 55, dy - 38, 110, 48,
limit_color,
line_color=rl.WHITE,
roundness=0.25,
segments=8,
line_thickness=2,
)
draw_text_ui_style(
str(disp_speed), dx, dy, 40, rl.WHITE,
font=self._font_display,
border_width=2.0,
shadow_offset=4.0,
align="center_bottom",
)
def _draw_carrot_main_background(self, bx: int, by: int):
show_device_state = ui_state.params.get_int("ShowDeviceState")
x_spd_limit, x_sign_type, _ = self._get_speed_limit_info()
cam_detected = x_spd_limit > 0 and x_sign_type not in (22, 4)
stroke_color = rl.WHITE
if cam_detected and self._blink_timer > 8:
bg_color = rl.Color(255, 0, 0, 180)
else:
bg_color = rl.Color(0, 0, 0, 90)
if show_device_state > 0:
self._draw_round_box(
bx - 120, by - 270, 475, 495,
bg_color,
line_color=stroke_color,
roundness=30.0 / 495.0,
segments=12,
line_thickness=2,
)
else:
self._draw_round_box(
bx - 120, by - 130, 475, 355,
bg_color,
line_color=stroke_color,
roundness=30.0 / 355.0,
segments=12,
line_thickness=2,
)
def _draw_carrot_device_state(self, bx: int, by: int):
show_device_state = ui_state.params.get_int("ShowDeviceState")
if show_device_state <= 0:
return
self._update_device_info()
dx = bx - 35
dy = by - 200
ok_color = rl.Color(0, 255, 0, 190)
# CPU
cpu_fill = rl.Color(255, 0, 0, 255) if (self._cpu_temp > 80 and self._blink_timer <= 8) else ok_color
self._draw_round_box(dx - 65, dy - 38, 130, 90, cpu_fill, line_color=rl.WHITE, roundness=0.16, segments=8, line_thickness=2)
draw_text_ui_style("CPU", dx, dy - 5, 25, rl.WHITE, font=self._font_display, border_width=1.0, shadow_offset=4.0, align="center_bottom")
draw_text_ui_style(f"{self._cpu_temp:.0f}°C", dx, dy + 40, 40, rl.WHITE, font=self._font_display, border_width=1.0, shadow_offset=4.0, align="center_bottom")
# MEM
dx2 = dx + 150
mem_fill = rl.Color(255, 0, 0, 255) if (self._memory_usage > 85 and self._blink_timer <= 8) else ok_color
self._draw_round_box(dx2 - 65, dy - 38, 130, 90, mem_fill, line_color=rl.WHITE, roundness=0.16, segments=8, line_thickness=2)
draw_text_ui_style("MEM", dx2, dy - 5, 25, rl.WHITE, font=self._font_display, border_width=1.0, shadow_offset=4.0, align="center_bottom")
draw_text_ui_style(f"{self._memory_usage}%", dx2, dy + 40, 40, rl.WHITE, font=self._font_display, border_width=1.0, shadow_offset=4.0, align="center_bottom")
# DISK / VOLT
dx3 = dx2 + 150
self._draw_round_box(dx3 - 65, dy - 38, 130, 90, ok_color, line_color=rl.WHITE, roundness=0.16, segments=8, line_thickness=2)
if self._disp_timer < 32:
draw_text_ui_style("DISK", dx3, dy - 5, 25, rl.WHITE, font=self._font_display, border_width=1.0, shadow_offset=4.0, align="center_bottom")
draw_text_ui_style(f"{100 - self._free_space:.0f}%", dx3, dy + 40, 40, rl.WHITE, font=self._font_display, border_width=1.0, shadow_offset=4.0, align="center_bottom")
else:
draw_text_ui_style("VOLT", dx3, dy - 5, 25, rl.WHITE, font=self._font_display, border_width=1.0, shadow_offset=4.0, align="center_bottom")
draw_text_ui_style(f"{self._voltage:.1f}V", dx3, dy + 40, 40, rl.WHITE, font=self._font_display, border_width=1.0, shadow_offset=4.0, align="center_bottom")
def _draw_date_time(self, rect: rl.Rectangle) -> None:
show_datetime = ui_state.params.get_int("ShowDateTime")
if show_datetime <= 0:
return
now = time.localtime()
weekdays_ko = ["", "", "", "", "", "", ""]
x = int(rect.x + 170)
y = int(rect.y + 120)
if show_datetime in (1, 2):
draw_text_ui_style(
time.strftime("%H:%M", now), x, y, 100, rl.WHITE,
font=self._font_display,
border_width=3.0,
shadow_offset=8.0,
align="center_bottom",
)
if show_datetime in (1, 3):
weekday = weekdays_ko[(now.tm_wday + 1) % 7]
date_text = f"{time.strftime('%m-%d', now)}({weekday})"
draw_text_ui_style(
date_text, x, y + 70, 60, rl.WHITE,
font=self._font_display,
border_width=3.0,
shadow_offset=8.0,
align="center_bottom",
)
def _get_tpms_color(self, tpms: float) -> rl.Color:
if tpms < 5 or tpms > 60:
return rl.Color(255, 255, 255, 220)
if tpms < 31:
return rl.Color(255, 90, 90, 220)
return rl.Color(255, 255, 255, 220)
def _get_tpms_text(self, tpms: float) -> str:
if tpms < 5 or tpms > 60:
return ' -'
return f'{round(tpms):.0f}'
def _draw_tpms_top_right(self, rect: rl.Rectangle) -> None:
show_tpms = 1 #ui_state.params.get_int('ShowTpms')
if show_tpms not in (1, 3):
return
try:
tpms = ui_state.sm['carState'].tpms
fl = float(tpms.fl)
fr = float(tpms.fr)
rl_v = float(tpms.rl)
rr = float(tpms.rr)
except Exception:
return
bx = rect.x + rect.width - 125
by = rect.y + 130
dw = 80
draw_text_ui_style(
self._get_tpms_text(fl), bx - dw, by - 55, 40, self._get_tpms_color(fl),
font=self._font_display, border_width=1.0, shadow_offset=4.0, align='center_bottom',
)
draw_text_ui_style(
self._get_tpms_text(fr), bx + dw, by - 55, 40, self._get_tpms_color(fr),
font=self._font_display, border_width=1.0, shadow_offset=4.0, align='center_bottom',
)
draw_text_ui_style(
self._get_tpms_text(rl_v), bx - dw, by + 70, 40, self._get_tpms_color(rl_v),
font=self._font_display, border_width=1.0, shadow_offset=4.0, align='center_bottom',
)
draw_text_ui_style(
self._get_tpms_text(rr), bx + dw, by + 70, 40, self._get_tpms_color(rr),
font=self._font_display, border_width=1.0, shadow_offset=4.0, align='center_bottom',
)
def _get_turn_info_hud_data(self) -> dict:
try:
cm = ui_state.sm["carrotMan"]
except Exception:
return {
"active_carrot": 0,
"x_turn_info": 0,
"x_dist_to_turn": 0,
"n_go_pos_dist": 0,
"n_go_pos_time": 0,
"atc_type": "",
"sdi_descr": "",
"road_name": "",
"tbt_main_text": "",
}
try:
active_carrot = int(cm.activeCarrot)
except Exception:
active_carrot = 0
try:
x_turn_info = int(cm.xTurnInfo)
except Exception:
x_turn_info = 0
try:
x_dist_to_turn = int(cm.xDistToTurn)
except Exception:
x_dist_to_turn = 0
try:
n_go_pos_dist = int(cm.nGoPosDist)
except Exception:
n_go_pos_dist = 0
try:
n_go_pos_time = int(cm.nGoPosTime)
except Exception:
n_go_pos_time = 0
try:
atc_type = str(cm.atcType or "")
except Exception:
atc_type = ""
try:
sdi_descr = str(cm.szSdiDescr or "")
except Exception:
sdi_descr = ""
try:
road_name = str(cm.szPosRoadName or "")
except Exception:
road_name = ""
try:
tbt_main_text = str(cm.szTBTMainText or "")
except Exception:
tbt_main_text = ""
return {
"active_carrot": active_carrot,
"x_turn_info": x_turn_info,
"x_dist_to_turn": x_dist_to_turn,
"n_go_pos_dist": n_go_pos_dist,
"n_go_pos_time": n_go_pos_time,
"atc_type": atc_type,
"sdi_descr": sdi_descr,
"road_name": road_name,
"tbt_main_text": tbt_main_text,
}
def _format_turn_distance_text(self, dist_m: int) -> str:
if dist_m <= 0:
return ""
if ui_state.is_metric:
if dist_m < 1000:
return f"{dist_m} m"
return f"{dist_m / 1000.0:.1f} km"
else:
if dist_m < 1609:
return f"{int(dist_m * 3.28084)} ft"
return f"{dist_m / 1609.344:.1f} mi"
def _format_eta_text(self, remain_sec: int) -> str:
if remain_sec <= 0:
return ""
eta_tm = time.localtime(time.time() + remain_sec)
remain_min = remain_sec / 60.0
return f"도착: {remain_min:.1f}분({eta_tm.tm_hour:02d}:{eta_tm.tm_min:02d})"
def _format_go_pos_distance_text(self, dist_m: int) -> str:
if dist_m <= 0:
return ""
if ui_state.is_metric:
return f"{dist_m / 1000.0:.1f}km"
else:
return f"{dist_m / 1000.0 * KM_TO_MILE:.1f}mile"
def _draw_text_left_bottom(self, text: str, x: float, y: float, size: int, color, font=None, border_width: float = 2.0, shadow_offset: float = 4.0):
if not text:
return
draw_text_ui_style(
text, x, y, size, color,
font=font or self._font_display,
border_width=border_width,
shadow_offset=shadow_offset,
align="left_bottom",
)
def _draw_turn_icon(self, turn_info: int, bx: int, by: int, icon_size: int = 140):
if turn_info == 1:
self._draw_texture_rect(self._ic_turn_l, bx - icon_size / 2, by - icon_size / 2, icon_size, icon_size)
elif turn_info == 2:
self._draw_texture_rect(self._ic_turn_r, bx - icon_size / 2, by - icon_size / 2, icon_size, icon_size)
elif turn_info == 3:
self._draw_texture_rect(self._ic_lane_change_l, bx - icon_size / 2, by - icon_size / 2, icon_size, icon_size)
elif turn_info == 4:
self._draw_texture_rect(self._ic_lane_change_r, bx - icon_size / 2, by - icon_size / 2, icon_size, icon_size)
elif turn_info == 7:
self._draw_texture_rect(self._ic_turn_u, bx - icon_size / 2, by - icon_size / 2, icon_size, icon_size)
elif turn_info == 6:
draw_text_ui_style(
"TG", bx, by + 20, 35, rl.WHITE,
font=self._font_display,
border_width=2.0,
shadow_offset=4.0,
align="center_bottom",
)
elif turn_info == 8:
draw_text_ui_style(
"목적지", bx, by + 20, 35, rl.WHITE,
font=self._font_display,
border_width=2.0,
shadow_offset=4.0,
align="center_bottom",
)
else:
draw_text_ui_style(
f"감속:{turn_info}", bx, by + 20, 35, rl.WHITE,
font=self._font_display,
border_width=2.0,
shadow_offset=4.0,
align="center_bottom",
)
def _draw_turn_info_hud(self, rect: rl.Rectangle):
if rect.width < 1200:
return
info = self._get_turn_info_hud_data()
n_go_pos_dist = info["n_go_pos_dist"]
n_go_pos_time = info["n_go_pos_time"]
if not (n_go_pos_dist > 0 and n_go_pos_time > 0):
return
tbt_x = int(rect.x + rect.width - 800)
tbt_y = int(rect.y + rect.height - 250)
self._draw_round_box(
tbt_x,
tbt_y - 60,
790,
300,
rl.Color(0, 0, 0, 120),
line_color=rl.WHITE,
roundness=30.0 / 300.0,
segments=12,
line_thickness=2,
)
if info["tbt_main_text"]:
self._draw_text_left_bottom(
info["tbt_main_text"], tbt_x + 20, tbt_y - 15, 40, rl.WHITE,
font=self._font_bold, border_width=2.0, shadow_offset=4.0,
)
x_turn_info = info["x_turn_info"]
x_dist_to_turn = info["x_dist_to_turn"]
if x_turn_info > 0:
bx = tbt_x + 100
by = tbt_y + 85
if info["atc_type"]:
fill_color = rl.Color(0, 255, 0, 100) if "prepare" in info["atc_type"] else rl.GREEN
self._draw_round_box(
bx - 80, by - 90, 160, 230,
fill_color,
line_color=rl.BLACK,
roundness=15.0 / 230.0,
segments=8,
line_thickness=1,
)
self._draw_turn_icon(x_turn_info, bx, by, 140)
dist_text = self._format_turn_distance_text(x_dist_to_turn)
if dist_text:
draw_text_ui_style(
dist_text,
bx, by + 120, 40, rl.WHITE,
font=self._font_bold,
border_width=2.0,
shadow_offset=4.0,
align="center_bottom",
)
if info["sdi_descr"]:
label_x = tbt_x + 200
label_y = tbt_y + 200
size = measure_text_cached(self._font_bold, info["sdi_descr"], 40)
box_h = max(48, int(size.y + 13))
self._draw_round_box(
label_x - 10,
label_y - int(size.y) - 2,
int(size.x) + 20,
box_h,
rl.GREEN,
roundness=10.0 / box_h,
segments=8,
line_thickness=0,
)
self._draw_text_left_bottom(
info["sdi_descr"], label_x, label_y, 40, rl.WHITE,
font=self._font_bold, border_width=1.5, shadow_offset=3.0,
)
elif info["road_name"]:
self._draw_text_left_bottom(
info["road_name"], tbt_x + 200, tbt_y + 200, 40, rl.WHITE,
font=self._font_bold, border_width=1.5, shadow_offset=3.0,
)
eta_text = self._format_eta_text(n_go_pos_time)
if eta_text:
self._draw_text_left_bottom(
eta_text, tbt_x + 190, tbt_y + 80, 50, rl.WHITE,
font=self._font_bold, border_width=2.0, shadow_offset=4.0,
)
go_dist_text = self._format_go_pos_distance_text(n_go_pos_dist)
if go_dist_text:
self._draw_text_left_bottom(
go_dist_text, tbt_x + 310, tbt_y + 130, 50, rl.WHITE,
font=self._font_bold, border_width=2.0, shadow_offset=4.0,
)
def _draw_set_speed_carrot(self, rect: rl.Rectangle) -> None:
self._blink_timer = (self._blink_timer + 1) % 16
self._disp_timer = (self._disp_timer + 1) % 64
# C drawHud anchor
bx = int(rect.x + 140)
by = int(rect.y + rect.height - 230)
self._draw_carrot_main_background(bx, by)
self._draw_carrot_traffic_light(bx, by)
self._draw_carrot_speed_panel(bx, by)
self._draw_carrot_lower_status(bx, by)
self._draw_carrot_speed_limit_box(bx, by)
self._draw_carrot_device_state(bx, by)
self._draw_turn_info_hud(rect)
class PlotRenderer:
PLOT_MAX = 400
def __init__(self):
self._plot_size = 0
self._plot_index = 0
self._plot_queue = [[0.0] * self.PLOT_MAX for _ in range(3)]
self._plot_min = 0.0
self._plot_max = 0.0
self._plot_x = 350.0
self._plot_width = 1000.0
self._plot_y = 40.0
self._plot_height = 300.0
self._plot_dx = 2.0
self._show_plot_mode_prev = -1
def _clear(self):
self._plot_size = 0
self._plot_index = 0
self._plot_min = 0.0
self._plot_max = 0.0
self._plot_queue = [[0.0] * self.PLOT_MAX for _ in range(3)]
def _make_plot_data(self, sm, show_plot_mode: int):
car_state = sm['carState']
lp = sm['longitudinalPlan']
car_control = sm['carControl']
controls_state = sm['controlsState']
a_ego = float(car_state.aEgo)
v_ego = float(car_state.vEgo)
accel = 0.0
try:
accel = float(lp.accels[0])
except Exception:
pass
speeds_0 = 0.0
try:
speeds_0 = float(lp.speeds[0])
except Exception:
pass
accel_out = 0.0
try:
accel_out = float(car_control.actuators.accel)
except Exception:
pass
if show_plot_mode in (0, 1):
return [a_ego, accel, accel_out], '1.Accel (Y:a_ego, G:a_target, O:a_out)'
if show_plot_mode == 2:
return [speeds_0, v_ego, a_ego], '2.Speed/Accel(Y:speed_0, G:v_ego, O:a_ego)'
if show_plot_mode == 3:
pos_32 = 0.0
vel_32 = 0.0
vel_0 = 0.0
try:
pos_32 = float(sm['modelV2'].position.x[32])
except Exception:
pass
try:
vel_32 = float(sm['modelV2'].velocity.x[32])
except Exception:
pass
try:
vel_0 = float(sm['modelV2'].velocity.x[0])
except Exception:
pass
return [pos_32, vel_32, vel_0], '3.Model(Y:pos_32, G:vel_32, O:vel_0)'
if show_plot_mode == 4:
a_lead_k = 0.0
v_rel = 0.0
try:
a_lead_k = float(sm['radarState'].leadOne.aLeadK)
except Exception:
pass
try:
v_rel = float(sm['radarState'].leadOne.vRel)
except Exception:
pass
return [accel, a_lead_k, v_rel], '4.Lead(Y:accel, G:a_lead, O:v_rel)'
if show_plot_mode == 5:
a_lead = 0.0
j_lead = 0.0
try:
a_lead = float(sm['radarState'].leadOne.aLead)
except Exception:
pass
try:
j_lead = float(sm['radarState'].leadOne.jLead)
except Exception:
pass
return [a_ego, a_lead, j_lead], '5.Lead(Y:a_ego, G:a_lead, O:j_lead)'
if show_plot_mode == 6:
actual_lat_accel = 0.0
desired_lat_accel = 0.0
output = 0.0
try:
actual_lat_accel = float(controls_state.lateralControlState.torqueState.actualLateralAccel) * 10.0
except Exception:
pass
try:
desired_lat_accel = float(controls_state.lateralControlState.torqueState.desiredLateralAccel) * 10.0
except Exception:
pass
try:
output = float(controls_state.lateralControlState.torqueState.output) * 10.0
except Exception:
pass
return [actual_lat_accel, desired_lat_accel, output], '6.Steer(Y:actual, G:desire, O:output)'
if show_plot_mode == 7:
actual_angle = float(car_state.steeringAngleDeg)
target_angle = 0.0
angle_offset = 0.0
try:
target_angle = float(car_control.actuators.steeringAngleDeg)
except Exception:
pass
try:
angle_offset = float(sm['liveParameters'].angleOffsetDeg) * 10.0
except Exception:
pass
return [actual_angle, target_angle, angle_offset], '7.SteerA (Y:Actual, G:Target, O:Offset*10)'
if show_plot_mode == 8:
curvature = 0.0
try:
curvature = float(car_control.actuators.curvature) * 10000.0
except Exception:
pass
return [curvature, curvature, curvature], '8.Curvature (*10000)'
return [0.0, 0.0, 0.0], 'no data'
def _update_plot_queue(self, plot_data):
self._plot_index = (self._plot_index + 1) % self.PLOT_MAX
for i in range(3):
self._plot_queue[i][self._plot_index] = float(plot_data[i])
if self._plot_size < self.PLOT_MAX:
self._plot_size += 1
self._plot_min = float('inf')
self._plot_max = float('-inf')
for i in range(3):
values = self._plot_queue[i][:self._plot_size] if self._plot_size < self.PLOT_MAX else self._plot_queue[i]
self._plot_min = min(self._plot_min, min(values))
self._plot_max = max(self._plot_max, max(values))
if self._plot_min == float('inf'):
self._plot_min = -2.0
if self._plot_max == float('-inf'):
self._plot_max = 2.0
if self._plot_min > -2.0:
self._plot_min = -2.0
if self._plot_max < 2.0:
self._plot_max = 2.0
def _draw_plotting(self, index: int, x_base: float, y_base: float, color, font):
if self._plot_size <= 0:
return
plot_range = self._plot_max - self._plot_min
plot_ratio = self._plot_height if plot_range < 1.0 else (self._plot_height / plot_range)
prev = None
latest_x = None
latest_y = None
latest_value = 0.0
for i in range(self._plot_size):
data = self._plot_queue[index][(self._plot_index - i + self.PLOT_MAX) % self.PLOT_MAX]
plot_y = y_base + self._plot_height - (data - self._plot_min) * plot_ratio
plot_x = x_base + (self._plot_size - i) * self._plot_dx
pt = rl.Vector2(plot_x, plot_y)
if prev is not None:
rl.draw_line_ex(prev, pt, 3.0, color)
else:
latest_x = plot_x
latest_y = plot_y
latest_value = data
prev = pt
if latest_x is not None and latest_y is not None:
draw_text_ui_style(
f'{latest_value:.2f}', latest_x + 50, latest_y + (40 if index > 0 else 0), 40, color,
font=font, border_width=2.0, shadow_offset=4.0, align='center_bottom',
)
def draw(self, rect: rl.Rectangle, font) -> None:
show_plot_mode = ui_state.params.get_int('ShowPlotMode')
if show_plot_mode == 0:
return
try:
if not ui_state.sm.alive['carState'] or not ui_state.sm.alive['longitudinalPlan']:
return
except Exception:
return
if show_plot_mode != self._show_plot_mode_prev:
self._clear()
self._show_plot_mode_prev = show_plot_mode
try:
plot_data, title = self._make_plot_data(ui_state.sm, show_plot_mode)
except Exception:
return
self._update_plot_queue(plot_data)
if rect.width < 1200:
return
x_base = rect.x + self._plot_x
y_base = rect.y + self._plot_y
colors = [rl.YELLOW, rl.GREEN, rl.Color(255, 165, 0, 255)]
for i in range(3):
self._draw_plotting(i, x_base, y_base, colors[i], font)
draw_text_ui_style(
title, x_base + 400, y_base - 20, 25, rl.WHITE,
font=font, border_width=2.0, shadow_offset=4.0, align='center_bottom',
)