""" 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 pyray as rl from openpilot.cereal import log from openpilot.selfdrive.ui import UI_BORDER_SIZE from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.sunnypilot.onroad.developer_ui import DeveloperUiState from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.lib.text_measure import measure_text_cached class CircularAlertsRenderer: def __init__(self): self._green_light_alert_img = gui_app.texture("../../sunnypilot/selfdrive/assets/images/green_light.png", 250, 250) self._lead_depart_alert_img = gui_app.texture("../../sunnypilot/selfdrive/assets/images/lead_depart.png", 250, 250) self._e2e_alert_display_timer = 0 self._e2e_alert_frame = 0 self._green_light_alert = False self._lead_depart_alert = False self._standstill_elapsed_time = 0.0 self._is_standstill = False self._alert_text = "" self._alert_img = None self._allow_e2e_alerts = False def update(self) -> None: sm = ui_state.sm lp_sp = sm['longitudinalPlanSP'] car_state = sm['carState'] self._green_light_alert = lp_sp.e2eAlerts.greenLightAlert self._lead_depart_alert = lp_sp.e2eAlerts.leadDepartAlert self._is_standstill = car_state.standstill if not ui_state.started: self._standstill_elapsed_time = 0.0 self._allow_e2e_alerts = sm['selfdriveState'].alertSize == log.SelfdriveState.AlertSize.none and \ sm.recv_frame['driverStateV2'] > ui_state.started_frame if self._green_light_alert or self._lead_depart_alert: self._e2e_alert_display_timer = 3 * gui_app.target_fps # reset onroad sleep timer for e2e alerts ui_state.reset_onroad_sleep_timer() if self._e2e_alert_display_timer > 0: self._e2e_alert_frame += 1 self._e2e_alert_display_timer -= 1 if self._green_light_alert: self._alert_text = "GREEN\nLIGHT" self._alert_img = self._green_light_alert_img elif self._lead_depart_alert: self._alert_text = "LEAD VEHICLE\nDEPARTING" self._alert_img = self._lead_depart_alert_img elif ui_state.standstill_timer and self._is_standstill: self._alert_img = None self._standstill_elapsed_time += 1.0 / gui_app.target_fps minute = int(self._standstill_elapsed_time / 60) second = int(self._standstill_elapsed_time - (minute * 60)) self._alert_text = f"{minute:01d}:{second:02d}" self._e2e_alert_frame += 1 else: self._e2e_alert_frame = 0 if not self._is_standstill: self._standstill_elapsed_time = 0.0 def render(self, rect: rl.Rectangle) -> None: if not self._allow_e2e_alerts or (self._e2e_alert_display_timer <= 0 and not (ui_state.standstill_timer and self._is_standstill)): return e2e_alert_size = 250 dev_ui_width_adjustment = 180 if ui_state.developer_ui in (DeveloperUiState.RIGHT, DeveloperUiState.BOTH) else 100 x = rect.x + rect.width - e2e_alert_size - dev_ui_width_adjustment - (UI_BORDER_SIZE * 3) y = rect.y + rect.height / 2 + 20 alert_rect = rl.Rectangle(x - e2e_alert_size, y - e2e_alert_size, e2e_alert_size * 2, e2e_alert_size * 2) center = rl.Vector2(alert_rect.x + alert_rect.width / 2, alert_rect.y + alert_rect.height / 2) # Pulse logic is_pulsing = (self._e2e_alert_frame % gui_app.target_fps) < (gui_app.target_fps / 2.5) # Standstill Timer (STOPPED) should be static white if self._e2e_alert_display_timer == 0 and ui_state.standstill_timer and self._is_standstill: frame_color = rl.Color(255, 255, 255, 75) else: frame_color = rl.Color(255, 255, 255, 75) if is_pulsing else rl.Color(0, 255, 0, 75) # Draw Circle rl.draw_circle_v(center, e2e_alert_size, rl.Color(0, 0, 0, 190)) # Draw Ring (Border) rl.draw_ring(center, e2e_alert_size - 7.5, e2e_alert_size + 7.5, 0, 360, 0, frame_color) # Draw Image if self._alert_img and self._e2e_alert_display_timer > 0: img_x = center.x - self._alert_img.width / 2 img_y = center.y - self._alert_img.height / 2 rl.draw_texture_ex(self._alert_img, rl.Vector2(img_x, img_y), 0.0, 1.0, rl.WHITE) # Draw Text txt_color = rl.Color(255, 255, 255, 255) if is_pulsing else rl.Color(0, 255, 0, 190) font = gui_app.font(FontWeight.BOLD) text_size = 48 spacing = 0 lines = self._alert_text.split('\n') # Position text at bottom of alert circle bottom_y = (alert_rect.y + alert_rect.height) - (alert_rect.height / 7) # Draw lines upwards from bottom current_y = bottom_y - (len(lines) * text_size * FONT_SCALE) if self._e2e_alert_display_timer == 0 and ui_state.standstill_timer and self._is_standstill: # Standstill Timer Text alert_alt_text = "STOPPED" top_text_size = 80 measure_top = measure_text_cached(font, alert_alt_text, top_text_size, spacing) top_y = alert_rect.y + alert_rect.height / 3.5 rl.draw_text_ex(font, alert_alt_text, rl.Vector2(center.x - measure_top.x / 2, top_y), top_text_size, spacing, rl.Color(255, 175, 3, 240)) # Timer timer_text_size = 100 measure_timer = measure_text_cached(font, self._alert_text, timer_text_size, spacing) timer_y = (alert_rect.y + alert_rect.height) - (alert_rect.height / 5) - measure_timer.y rl.draw_text_ex(font, self._alert_text, rl.Vector2(center.x - measure_timer.x / 2, timer_y), timer_text_size, spacing, rl.WHITE) else: for line in lines: measure = measure_text_cached(font, line, text_size, spacing) line_x = center.x - measure.x / 2 rl.draw_text_ex(font, line, rl.Vector2(line_x, current_y), text_size, spacing, txt_color) current_y += text_size * FONT_SCALE