version: sunnypilot v2026.002.000 (staging) date: 2026-07-21T07:44:02 master commit: 2c334ede443d7391d27575af8c854a095ba702a8
148 lines
5.0 KiB
Python
148 lines
5.0 KiB
Python
"""
|
|
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
|
|
|
This file is part of sunnypilot and is licensed under the MIT License.
|
|
See the LICENSE.md file in the root directory for more details.
|
|
"""
|
|
import numpy as np
|
|
|
|
import pyray as rl
|
|
from openpilot.common.constants import CV
|
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
|
|
|
|
|
class ChevronOptions:
|
|
OFF = 0
|
|
DISTANCE_ONLY = 1
|
|
SPEED_ONLY = 2
|
|
TTC_ONLY = 3
|
|
ALL = 4
|
|
|
|
|
|
class ChevronMetrics:
|
|
def __init__(self):
|
|
self._lead_status_alpha: float = 0.0
|
|
self._font = gui_app.font(FontWeight.SEMI_BOLD)
|
|
|
|
def update_alpha(self, has_lead: bool):
|
|
"""Update the alpha value for fade in/out animation"""
|
|
if not has_lead:
|
|
self._lead_status_alpha = max(0.0, self._lead_status_alpha - 0.05)
|
|
else:
|
|
self._lead_status_alpha = min(1.0, self._lead_status_alpha + 0.1)
|
|
|
|
def should_render(self) -> bool:
|
|
"""Check if dev UI should be rendered"""
|
|
return ui_state.chevron_metrics != ChevronOptions.OFF and self._lead_status_alpha > 0.0
|
|
|
|
def _draw_lead(self, lead_data, lead_vehicle, v_ego: float, rect: rl.Rectangle):
|
|
"""Draw lead vehicle status information (distance, speed, TTC)"""
|
|
if not self.should_render():
|
|
return
|
|
|
|
d_rel = lead_data.dRel
|
|
v_rel = lead_data.vRel
|
|
|
|
if not lead_vehicle.chevron or len(lead_vehicle.chevron) < 2:
|
|
return
|
|
|
|
chevron_x = lead_vehicle.chevron[1][0]
|
|
chevron_y = lead_vehicle.chevron[1][1]
|
|
sz = np.clip((25 * 30) / (d_rel / 3 + 30), 15.0, 30.0) * 2.35
|
|
|
|
text_lines = self._build_text_lines(d_rel, v_rel, v_ego)
|
|
if not text_lines:
|
|
return
|
|
|
|
self._render_text_lines(text_lines, chevron_x, chevron_y, sz, rect)
|
|
|
|
@staticmethod
|
|
def _build_text_lines(d_rel: float, v_rel: float, v_ego: float) -> list[str]:
|
|
"""Build text lines based on chevron info setting"""
|
|
text_lines = []
|
|
|
|
# Distance
|
|
if ui_state.chevron_metrics == ChevronOptions.DISTANCE_ONLY or ui_state.chevron_metrics == ChevronOptions.ALL:
|
|
val = max(0.0, d_rel)
|
|
unit = "m" if ui_state.is_metric else "ft"
|
|
if not ui_state.is_metric:
|
|
val *= 3.28084
|
|
text_lines.append(f"{val:.0f} {unit}")
|
|
|
|
# Speed
|
|
if ui_state.chevron_metrics == ChevronOptions.SPEED_ONLY or ui_state.chevron_metrics == ChevronOptions.ALL:
|
|
multiplier = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
|
|
val = max(0.0, (v_rel + v_ego) * multiplier)
|
|
unit = "km/h" if ui_state.is_metric else "mph"
|
|
text_lines.append(f"{val:.0f} {unit}")
|
|
|
|
# Time to collision
|
|
if ui_state.chevron_metrics == ChevronOptions.TTC_ONLY or ui_state.chevron_metrics == ChevronOptions.ALL:
|
|
val = (d_rel / v_ego) if (d_rel > 0 and v_ego > 0) else 0.0
|
|
ttc_text = f"{val:.1f} s" if (0 < val < 200) else "---"
|
|
text_lines.append(ttc_text)
|
|
|
|
return text_lines
|
|
|
|
def _render_text_lines(self, text_lines: list[str], chevron_x: float, chevron_y: float,
|
|
sz: float, rect: rl.Rectangle):
|
|
"""Render text lines with proper centering and positioning"""
|
|
font_size = 40
|
|
line_height = 50
|
|
margin = 20
|
|
|
|
text_y = chevron_y + sz + 15
|
|
total_height = len(text_lines) * line_height
|
|
|
|
# Adjust Y position if text would go off screen
|
|
if text_y + total_height > rect.height - margin:
|
|
y_max = min(chevron_y, rect.height - margin)
|
|
text_y = y_max - 15 - total_height
|
|
text_y = max(margin, text_y)
|
|
|
|
alpha = int(255 * self._lead_status_alpha)
|
|
text_color = rl.Color(255, 255, 255, alpha)
|
|
shadow_color = rl.Color(0, 0, 0, int(200 * self._lead_status_alpha))
|
|
|
|
for i, line in enumerate(text_lines):
|
|
y = int(text_y + (i * line_height))
|
|
if y + line_height > rect.height - margin:
|
|
break
|
|
|
|
# Measure actual text width for proper centering
|
|
text_size = measure_text_cached(self._font, line, font_size, 0)
|
|
text_width = text_size.x
|
|
|
|
# Center the text horizontally on the chevron
|
|
x = int(chevron_x - text_width / 2)
|
|
x = int(np.clip(x, margin, rect.width - text_width - margin))
|
|
|
|
# Draw shadow
|
|
rl.draw_text_ex(self._font, line, rl.Vector2(x + 2, y + 2), font_size, 0, shadow_color)
|
|
# Draw text
|
|
rl.draw_text_ex(self._font, line, rl.Vector2(x, y), font_size, 0, text_color)
|
|
|
|
def draw_lead_status(self, sm, radar_state, rect, lead_vehicles):
|
|
lead_one = radar_state.leadOne
|
|
lead_two = radar_state.leadTwo
|
|
|
|
has_lead_one = lead_one.present if lead_one else False
|
|
has_lead_two = lead_two.present if lead_two else False
|
|
|
|
self.update_alpha(has_lead_one or has_lead_two)
|
|
|
|
if not self.should_render():
|
|
return
|
|
|
|
v_ego = sm['carState'].vEgo
|
|
|
|
if has_lead_one and lead_vehicles[0].chevron:
|
|
self._draw_lead(lead_one, lead_vehicles[0], v_ego, rect)
|
|
|
|
if has_lead_two and lead_vehicles[1].chevron:
|
|
d_rel_diff = abs(lead_one.dRel - lead_two.dRel) if has_lead_one else float('inf')
|
|
if d_rel_diff > 3.0:
|
|
self._draw_lead(lead_two, lead_vehicles[1], v_ego, rect)
|