import pyray as rl
import time
from dataclasses import dataclass
from collections . abc import Callable
from cereal import log
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
SIDEBAR_WIDTH = 300
METRIC_HEIGHT = 126
METRIC_WIDTH = 240
METRIC_MARGIN = 30
SETTINGS_BTN = rl . Rectangle ( 50 , 35 , 200 , 117 )
HOME_BTN = rl . Rectangle ( 60 , 860 , 180 , 180 )
ThermalStatus = log . DeviceState . ThermalStatus
NetworkType = log . DeviceState . NetworkType
# Color scheme
class Colors :
SIDEBAR_BG = rl . Color ( 57 , 57 , 57 , 255 )
WHITE = rl . Color ( 255 , 255 , 255 , 255 )
WHITE_DIM = rl . Color ( 255 , 255 , 255 , 85 )
GRAY = rl . Color ( 84 , 84 , 84 , 255 )
# Status colors
GOOD = rl . Color ( 255 , 255 , 255 , 255 )
WARNING = rl . Color ( 218 , 202 , 37 , 255 )
DANGER = rl . Color ( 201 , 34 , 49 , 255 )
# UI elements
METRIC_BORDER = rl . Color ( 255 , 255 , 255 , 85 )
BUTTON_NORMAL = rl . Color ( 255 , 255 , 255 , 255 )
BUTTON_PRESSED = rl . Color ( 255 , 255 , 255 , 166 )
NETWORK_TYPES = {
NetworkType . none : " Offline " ,
NetworkType . wifi : " WiFi " ,
NetworkType . cell2G : " 2G " ,
NetworkType . cell3G : " 3G " ,
NetworkType . cell4G : " LTE " ,
NetworkType . cell5G : " 5G " ,
NetworkType . ethernet : " Ethernet " ,
}
@dataclass ( slots = True )
class MetricData :
label : str
value : str
color : rl . Color
def update ( self , label : str , value : str , color : rl . Color ) :
self . label = label
self . value = value
self . color = color
class Sidebar :
def __init__ ( self ) :
self . _net_type = NETWORK_TYPES . get ( NetworkType . none )
self . _net_strength = 0
self . _temp_status = MetricData ( " TEMP " , " GOOD " , Colors . GOOD )
self . _panda_status = MetricData ( " VEHICLE " , " ONLINE " , Colors . GOOD )
self . _connect_status = MetricData ( " CONNECT " , " OFFLINE " , Colors . WARNING )
self . _home_img = gui_app . texture ( " images/button_home.png " , HOME_BTN . width , HOME_BTN . height )
self . _flag_img = gui_app . texture ( " images/button_flag.png " , HOME_BTN . width , HOME_BTN . height )
self . _settings_img = gui_app . texture ( " images/button_settings.png " , SETTINGS_BTN . width , SETTINGS_BTN . height )
self . _font_regular = gui_app . font ( FontWeight . NORMAL )
self . _font_bold = gui_app . font ( FontWeight . SEMI_BOLD )
# Callbacks
self . _on_settings_click : Callable | None = None
self . _on_flag_click : Callable | None = None
def set_callbacks ( self , on_settings : Callable | None = None , on_flag : Callable | None = None ) :
self . _on_settings_click = on_settings
self . _on_flag_click = on_flag
def render ( self , rect : rl . Rectangle ) :
self . update_state ( )
# Background
rl . draw_rectangle_rec ( rect , Colors . SIDEBAR_BG )
self . _draw_buttons ( rect )
self . _draw_network_indicator ( rect )
self . _draw_metrics ( rect )
self . _handle_mouse_release ( )
def update_state ( self ) :
sm = ui_state . sm
if not sm . updated [ ' deviceState ' ] :
return
device_state = sm [ ' deviceState ' ]
self . _update_network_status ( device_state )
self . _update_temperature_status ( device_state )
self . _update_connection_status ( device_state )
self . _update_panda_status ( )
def _update_network_status ( self , device_state ) :
self . _net_type = NETWORK_TYPES . get ( device_state . networkType . raw , " Unknown " )
strength = device_state . networkStrength
self . _net_strength = max ( 0 , min ( 5 , strength . raw + 1 ) ) if strength > 0 else 0
def _update_temperature_status ( self , device_state ) :
thermal_status = device_state . thermalStatus
if thermal_status == ThermalStatus . green :
self . _temp_status . update ( " TEMP " , " GOOD " , Colors . GOOD )
elif thermal_status == ThermalStatus . yellow :
self . _temp_status . update ( " TEMP " , " OK " , Colors . WARNING )
else :
self . _temp_status . update ( " TEMP " , " HIGH " , Colors . DANGER )
def _update_connection_status ( self , device_state ) :
last_ping = device_state . lastAthenaPingTime
if last_ping == 0 :
self . _connect_status . update ( " CONNECT " , " OFFLINE " , Colors . WARNING )
elif time . monotonic_ns ( ) - last_ping < 80_000_000_000 : # 80 seconds in nanoseconds
self . _connect_status . update ( " CONNECT " , " ONLINE " , Colors . GOOD )
else :
self . _connect_status . update ( " CONNECT " , " ERROR " , Colors . DANGER )
def _update_panda_status ( self ) :
if ui_state . panda_type == log . PandaState . PandaType . unknown :
self . _panda_status . update ( " NO " , " PANDA " , Colors . DANGER )
else :
self . _panda_status . update ( " VEHICLE " , " ONLINE " , Colors . GOOD )
def _handle_mouse_release ( self ) :
if not rl . is_mouse_button_released ( rl . MouseButton . MOUSE_BUTTON_LEFT ) :
return
mouse_pos = rl . get_mouse_position ( )
if rl . check_collision_point_rec ( mouse_pos , SETTINGS_BTN ) :
if self . _on_settings_click :
self . _on_settings_click ( )
elif rl . check_collision_point_rec ( mouse_pos , HOME_BTN ) and ui_state . started :
if self . _on_flag_click :
self . _on_flag_click ( )
def _draw_buttons ( self , rect : rl . Rectangle ) :
mouse_pos = rl . get_mouse_position ( )
mouse_down = rl . is_mouse_button_down ( rl . MouseButton . MOUSE_BUTTON_LEFT )
# Settings button
settings_down = mouse_down and rl . check_collision_point_rec ( mouse_pos , SETTINGS_BTN )
tint = Colors . BUTTON_PRESSED if settings_down else Colors . BUTTON_NORMAL
rl . draw_texture ( self . _settings_img , int ( SETTINGS_BTN . x ) , int ( SETTINGS_BTN . y ) , tint )
# Home/Flag button
flag_pressed = mouse_down and rl . check_collision_point_rec ( mouse_pos , HOME_BTN )
button_img = self . _flag_img if ui_state . started else self . _home_img
tint = Colors . BUTTON_PRESSED if ( ui_state . started and flag_pressed ) else Colors . BUTTON_NORMAL
rl . draw_texture ( button_img , int ( HOME_BTN . x ) , int ( HOME_BTN . y ) , tint )
def _draw_network_indicator ( self , rect : rl . Rectangle ) :
# Signal strength dots
x_start = rect . x + 58
y_pos = rect . y + 196
dot_size = 27
dot_spacing = 37
for i in range ( 5 ) :
color = Colors . WHITE if i < self . _net_strength else Colors . GRAY
x = int ( x_start + i * dot_spacing + dot_size / / 2 )
y = int ( y_pos + dot_size / / 2 )
rl . draw_circle ( x , y , dot_size / / 2 , color )
# Network type text
text_y = rect . y + 247
text_pos = rl . Vector2 ( rect . x + 58 , text_y )
rl . draw_text_ex ( self . _font_regular , self . _net_type , text_pos , 35 , 0 , Colors . WHITE )
def _draw_metrics ( self , rect : rl . Rectangle ) :
metrics = [ ( self . _temp_status , 338 ) , ( self . _panda_status , 496 ) , ( self . _connect_status , 654 ) ]
for metric , y_offset in metrics :
self . _draw_metric ( rect , metric , rect . y + y_offset )
def _draw_metric ( self , rect : rl . Rectangle , metric : MetricData , y : float ) :
metric_rect = rl . Rectangle ( rect . x + METRIC_MARGIN , y , METRIC_WIDTH , METRIC_HEIGHT )
# Draw colored left edge (clipped rounded rectangle)
edge_rect = rl . Rectangle ( metric_rect . x + 4 , metric_rect . y + 4 , 100 , 118 )
rl . begin_scissor_mode ( int ( metric_rect . x + 4 ) , int ( metric_rect . y ) , 18 , int ( metric_rect . height ) )
rl . draw_rectangle_rounded ( edge_rect , 0.18 , 10 , metric . color )
rl . end_scissor_mode ( )
# Draw border
rl . draw_rectangle_rounded_lines_ex ( metric_rect , 0.15 , 10 , 2 , Colors . METRIC_BORDER )
# Draw text
text = f " { metric . label } \n { metric . value } "
text_size = measure_text_cached ( self . _font_bold , text , 35 )
text_pos = rl . Vector2 (
metric_rect . x + 22 + ( metric_rect . width - 22 - text_size . x ) / 2 ,
metric_rect . y + ( metric_rect . height - text_size . y ) / 2
)
rl . draw_text_ex ( self . _font_bold , text , text_pos , 35 , 0 , Colors . WHITE )