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
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)
|
|
|