openpilot is an open source driver assistance system. openpilot performs the functions of Automated Lane Centering and Adaptive Cruise Control for over 200 supported car makes and models.
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.
 
 
 
 
 
 

208 lines
7.3 KiB

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)