54e6124925
version: sunnypilot v2026.001.000 (dev) date: 2026-05-07T23:07:19 master commit: c28eb958740187620f2282023b8f1997cf90f583
193 lines
7.4 KiB
Python
193 lines
7.4 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.
|
|
"""
|
|
from enum import IntEnum
|
|
|
|
import pyray as rl
|
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
|
from openpilot.selfdrive.ui.sunnypilot.onroad.developer_ui.elements import (
|
|
UiElement, RelDistElement, RelSpeedElement, SteeringAngleElement,
|
|
DesiredLateralAccelElement, ActualLateralAccelElement, DesiredSteeringAngleElement,
|
|
AEgoElement, LeadSpeedElement, FrictionCoefficientElement, LatAccelFactorElement,
|
|
SteeringTorqueEpsElement, BearingDegElement, AltitudeElement, DesiredSteeringPIDElement
|
|
)
|
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
|
from openpilot.system.ui.widgets import Widget
|
|
|
|
|
|
def get_bottom_dev_ui_offset():
|
|
if ui_state.developer_ui in (DeveloperUiState.BOTTOM, DeveloperUiState.BOTH):
|
|
return 60
|
|
return 0
|
|
|
|
|
|
class DeveloperUiState(IntEnum):
|
|
OFF = 0
|
|
BOTTOM = 1
|
|
RIGHT = 2
|
|
BOTH = 3
|
|
|
|
|
|
class DeveloperUiRenderer(Widget):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
|
|
self._font_semi_bold: rl.Font = gui_app.font(FontWeight.SEMI_BOLD)
|
|
self.dev_ui_mode = DeveloperUiState.OFF
|
|
|
|
self.rel_dist_elem = RelDistElement()
|
|
self.rel_speed_elem = RelSpeedElement()
|
|
self.steering_angle_elem = SteeringAngleElement()
|
|
self.desired_lat_accel_elem = DesiredLateralAccelElement()
|
|
self.actual_lat_accel_elem = ActualLateralAccelElement()
|
|
self.desired_steer_elem = DesiredSteeringAngleElement()
|
|
self.desired_pid_steer_elem = DesiredSteeringPIDElement()
|
|
self.a_ego_elem = AEgoElement()
|
|
self.lead_speed_elem = LeadSpeedElement()
|
|
self.friction_elem = FrictionCoefficientElement()
|
|
self.lat_accel_factor_elem = LatAccelFactorElement()
|
|
self.steering_torque_elem = SteeringTorqueEpsElement()
|
|
self.bearing_elem = BearingDegElement()
|
|
self.altitude_elem = AltitudeElement()
|
|
|
|
def _update_state(self) -> None:
|
|
self.dev_ui_mode = ui_state.developer_ui
|
|
|
|
def _render(self, rect: rl.Rectangle) -> None:
|
|
if self.dev_ui_mode == DeveloperUiState.OFF:
|
|
return
|
|
|
|
sm = ui_state.sm
|
|
if sm.recv_frame["carState"] < ui_state.started_frame:
|
|
return
|
|
|
|
if self.dev_ui_mode == DeveloperUiState.BOTTOM:
|
|
self._draw_bottom_dev_ui(rect)
|
|
elif self.dev_ui_mode == DeveloperUiState.RIGHT:
|
|
self._draw_right_dev_ui(rect)
|
|
elif self.dev_ui_mode == DeveloperUiState.BOTH:
|
|
self._draw_right_dev_ui(rect)
|
|
self._draw_bottom_dev_ui(rect)
|
|
|
|
def _draw_right_dev_ui(self, rect: rl.Rectangle) -> None:
|
|
sm = ui_state.sm
|
|
controls_state = sm['controlsState']
|
|
|
|
UI_BORDER_SIZE = 20
|
|
container_width = 184
|
|
x = int(rect.x + rect.width - container_width - UI_BORDER_SIZE * 2)
|
|
y = int(rect.y + UI_BORDER_SIZE * 1.5)
|
|
|
|
elements = [
|
|
self.rel_dist_elem.update(sm, ui_state.is_metric),
|
|
self.rel_speed_elem.update(sm, ui_state.is_metric),
|
|
self.steering_angle_elem.update(sm, ui_state.is_metric),
|
|
]
|
|
if controls_state.lateralControlState.which() == 'torqueState':
|
|
elements.append(self.desired_lat_accel_elem.update(sm, ui_state.is_metric))
|
|
elif controls_state.lateralControlState.which() == 'angleState':
|
|
elements.append(self.desired_steer_elem.update(sm, ui_state.is_metric))
|
|
elif controls_state.lateralControlState.which() == 'pidState':
|
|
elements.append(self.desired_pid_steer_elem.update(sm, ui_state.is_metric))
|
|
|
|
elements.append(self.actual_lat_accel_elem.update(sm, ui_state.is_metric))
|
|
|
|
current_y = y
|
|
for element in elements:
|
|
current_y += self._draw_right_dev_ui_element(x, current_y, element)
|
|
|
|
def _draw_right_dev_ui_element(self, x: int, y: int, element: UiElement) -> int:
|
|
x += 0
|
|
y += 230
|
|
container_width = 184
|
|
label_size = 28
|
|
value_size = 60
|
|
unit_size = 28
|
|
label_width = measure_text_cached(self._font_bold, element.label, label_size, 0).x
|
|
centered_label_x = x + (container_width - label_width) / 2
|
|
rl.draw_text_ex(self._font_bold, element.label, rl.Vector2(centered_label_x, y), label_size, 0, rl.WHITE)
|
|
|
|
y += 45
|
|
value_width = measure_text_cached(self._font_bold, element.value, value_size, 0).x
|
|
centered_value_x = x + (container_width - value_width) / 2
|
|
rl.draw_text_ex(self._font_bold, element.value, rl.Vector2(centered_value_x, y), value_size, 0, element.color)
|
|
|
|
if element.unit:
|
|
units_height = measure_text_cached(self._font_bold, element.unit, unit_size, 0).x
|
|
|
|
units_x = x + container_width
|
|
units_y = y + (value_size / 2) + (units_height / 2)
|
|
|
|
rl.draw_text_pro(self._font_bold, element.unit, rl.Vector2(units_x, units_y), rl.Vector2(0, 0), -90.0, unit_size, 0, rl.WHITE)
|
|
|
|
return 130
|
|
|
|
def _draw_bottom_dev_ui(self, rect: rl.Rectangle) -> None:
|
|
sm = ui_state.sm
|
|
bar_height = 61
|
|
y = int(rect.y + rect.height - bar_height)
|
|
|
|
rl.draw_rectangle(int(rect.x), y, int(rect.width), bar_height,
|
|
rl.Color(0, 0, 0, 100))
|
|
|
|
elements = [
|
|
self.a_ego_elem.update(sm, ui_state.is_metric),
|
|
self.lead_speed_elem.update(sm, ui_state.is_metric),
|
|
]
|
|
|
|
# Add torque-specific elements if using torque control
|
|
if sm['controlsState'].lateralControlState.which() == 'torqueState':
|
|
override_active = ui_state.enforce_torque_control and ui_state.custom_torque_params and ui_state.torque_override_enabled
|
|
if sm.valid['liveTorqueParameters'] or override_active:
|
|
elements.extend([
|
|
self.friction_elem.update(sm, ui_state.is_metric),
|
|
self.lat_accel_factor_elem.update(sm, ui_state.is_metric),
|
|
])
|
|
else:
|
|
# Non-torque: show steering torque and GPS data
|
|
elements.append(self.steering_torque_elem.update(sm, ui_state.is_metric))
|
|
|
|
if sm.valid['gpsLocationExternal'] or sm.valid['gpsLocation']:
|
|
elements.append(self.bearing_elem.update(sm, ui_state.is_metric))
|
|
|
|
# Add altitude if GPS available
|
|
if sm.valid['gpsLocationExternal'] or sm.valid['gpsLocation']:
|
|
elements.append(self.altitude_elem.update(sm, ui_state.is_metric))
|
|
|
|
if not elements:
|
|
return
|
|
|
|
font_size = 38
|
|
element_widths = []
|
|
for element in elements:
|
|
element.measure(self._font_bold, font_size)
|
|
element_widths.append(element.total_width)
|
|
|
|
total_element_width = sum(element_widths)
|
|
num_gaps = len(elements) + 1
|
|
available_width = rect.width
|
|
gap_width = (available_width - total_element_width) / num_gaps
|
|
|
|
center_y = y + bar_height // 2
|
|
current_x = rect.x + gap_width
|
|
|
|
for i, element in enumerate(elements):
|
|
element_center_x = int(current_x + element_widths[i] / 2)
|
|
self._draw_bottom_dev_ui_element(element_center_x, center_y, element)
|
|
current_x += element_widths[i] + gap_width
|
|
|
|
def _draw_bottom_dev_ui_element(self, center_x: int, y: int, element: UiElement) -> None:
|
|
font_size = 38
|
|
start_x = center_x - element.total_width / 2
|
|
|
|
rl.draw_text_ex(self._font_bold, element.label_text, rl.Vector2(start_x, y - font_size // 2), font_size, 0, rl.WHITE)
|
|
rl.draw_text_ex(self._font_bold, element.val_text, rl.Vector2(start_x + element.label_width, y - font_size // 2), font_size, 0, element.color)
|
|
|
|
if element.unit:
|
|
rl.draw_text_ex(self._font_bold, element.unit_text, rl.Vector2(start_x + element.label_width + element.val_width, y - font_size // 2),
|
|
font_size, 0, rl.WHITE)
|