Files
StarPilot/selfdrive/ui/onroad/starpilot/developer_sidebar.py
T
2026-05-21 04:37:41 -04:00

278 lines
11 KiB
Python

import pyray as rl
import time
import re
from cereal import car
from openpilot.common.params import Params
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
from openpilot.system.ui.lib.text_measure import measure_text_cached
SIDEBAR_WIDTH = 300
METRIC_HEIGHT = 126
METRIC_WIDTH = 275
METRIC_MARGIN = 12
FONT_SIZE = 35
METER_TO_FOOT = 3.28084
_WHITE_DIM = rl.Color(255, 255, 255, 85)
def parse_hex_color(hex_str: str, default_color=rl.WHITE) -> rl.Color:
if not hex_str:
return default_color
hex_str = hex_str.lstrip('#')
try:
if len(hex_str) == 6:
r = int(hex_str[0:2], 16)
g = int(hex_str[2:4], 16)
b = int(hex_str[4:6], 16)
return rl.Color(r, g, b, 255)
elif len(hex_str) == 8:
r = int(hex_str[0:2], 16)
g = int(hex_str[2:4], 16)
b = int(hex_str[4:6], 16)
a = int(hex_str[6:8], 16)
return rl.Color(r, g, b, a)
except ValueError:
pass
return default_color
class DeveloperSidebar:
def __init__(self):
self._params = Params()
self._font_bold = gui_app.font(FontWeight.SEMI_BOLD)
self._last_toggles_check = 0.0
self._cached_sidebar = False
self._cached_metrics = [0] * 7
self._cached_force_auto_tune_off = False
self._cached_force_auto_tune = False
self._cached_friction_stock = 0.0
self._cached_friction = 0.0
self._cached_lat_stock = 0.0
self._cached_lat = 0.0
self.lateral_engagement_time = 0
self.longitudinal_engagement_time = 0
self.total_engagement_time = 0
self.max_acceleration = 0.0
self.max_steer_angle = 0
self.max_torque = 0
self.torque_timer_start = 0.0
self._visible = False
self._metric_color = rl.WHITE
self._active_ids: list[int] = []
self._metrics: dict[int, tuple[str, str]] = {}
@property
def visible(self) -> bool:
return self._visible
def reset_variables(self):
self.lateral_engagement_time = 0
self.longitudinal_engagement_time = 0
self.total_engagement_time = 0
self.max_acceleration = 0.0
self.max_steer_angle = 0
self.max_torque = 0
self.torque_timer_start = 0.0
def _refresh_cache(self):
now = time.monotonic()
if now - self._last_toggles_check < 1.0:
return
self._last_toggles_check = now
self._cached_sidebar = self._params.get_bool("DeveloperSidebar")
self._cached_metrics = [self._params.get_int(f"DeveloperSidebarMetric{i}") for i in range(1, 8)]
self._cached_force_auto_tune_off = self._params.get_bool("ForceAutoTuneOff")
self._cached_force_auto_tune = self._params.get_bool("ForceAutoTune")
self._cached_friction_stock = self._params.get_float("SteerFrictionStock")
self._cached_friction = self._params.get_float("SteerFriction")
self._cached_lat_stock = self._params.get_float("SteerLatAccelStock")
self._cached_lat = self._params.get_float("SteerLatAccel")
def _draw_metric(self, sidebar_rect: rl.Rectangle, label_first: str, label_second: str, color: rl.Color, y: float):
card_x = int(sidebar_rect.x + sidebar_rect.width) - METRIC_MARGIN - METRIC_WIDTH
metric_rect = rl.Rectangle(card_x, y, METRIC_WIDTH, METRIC_HEIGHT)
edge_rect = rl.Rectangle(metric_rect.x + METRIC_WIDTH - 4 - 100, metric_rect.y + 4, 100, 118)
rl.begin_scissor_mode(
int(metric_rect.x + METRIC_WIDTH - 4 - 18),
int(metric_rect.y),
18,
int(metric_rect.height)
)
rl.draw_rectangle_rounded(edge_rect, 0.3, 10, color)
rl.end_scissor_mode()
rl.draw_rectangle_rounded_lines_ex(metric_rect, 0.3, 10, 2, _WHITE_DIM)
if label_second == "":
text_size = measure_text_cached(self._font_bold, label_first, FONT_SIZE)
text_pos = rl.Vector2(
metric_rect.x + (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, label_first, text_pos, FONT_SIZE, 0, rl.WHITE)
else:
labels = [label_first, label_second]
text_y = metric_rect.y + (metric_rect.height / 2 - len(labels) * FONT_SIZE * FONT_SCALE)
for text in labels:
text_size = measure_text_cached(self._font_bold, text, FONT_SIZE)
text_y += text_size.y
text_pos = rl.Vector2(
metric_rect.x + (metric_rect.width - 22 - text_size.x) / 2,
text_y
)
rl.draw_text_ex(self._font_bold, text, text_pos, FONT_SIZE, 0, rl.WHITE)
def update(self):
self._refresh_cache()
self._visible = (
self._cached_sidebar or
ui_state.starpilot_toggles.get("developer_sidebar", False)
)
if not self._visible:
return
if ui_state.sm.frame < ui_state.started_frame + 2:
self.reset_variables()
assignments = []
for i, val in enumerate(self._cached_metrics):
if val == 0:
val = ui_state.starpilot_toggles.get(f"developer_sidebar_metric{i + 1}", 0)
assignments.append(val)
color_str = ui_state.starpilot_toggles.get("sidebar_color1", "#FFFFFFFF")
self._metric_color = parse_hex_color(color_str)
self._active_ids = [m for m in assignments if m > 0]
if len(self._active_ids) == 0:
return
sm = ui_state.sm
car_state = sm["carState"] if sm.valid.get("carState", False) else None
car_control = sm["carControl"] if sm.valid.get("carControl", False) else None
starpilot_plan = sm["starpilotPlan"] if sm.valid.get("starpilotPlan", False) else None
live_delay = sm["liveDelay"] if sm.valid.get("liveDelay", False) else None
live_parameters = sm["liveParameters"] if sm.valid.get("liveParameters", False) else None
live_torque_parameters = sm["liveTorqueParameters"] if sm.valid.get("liveTorqueParameters", False) else None
is_metric = ui_state.is_metric
use_si = ui_state.starpilot_toggles.get("use_si_metrics", False)
accel_unit = " m/s²" if (is_metric or use_si) else " ft/s²"
accel_conv = 1.0 if (is_metric or use_si) else METER_TO_FOOT
a_ego = car_state.aEgo if car_state else 0.0
accel_val = a_ego * accel_conv
gas_pressed = car_state.gasPressed if car_state else False
if not gas_pressed:
self.max_acceleration = max(self.max_acceleration, accel_val)
lat_active = car_control.latActive if car_control else False
long_active = car_control.longActive if car_control else False
standstill = car_state.standstill if car_state else False
reverse = car_state.gearShifter == car.CarState.GearShifter.reverse if car_state else False
self.lateral_engagement_time += 1 if (lat_active and not standstill and not reverse) else 0
self.longitudinal_engagement_time += 1 if (long_active and not standstill and not reverse) else 0
self.total_engagement_time += 1 if ((not standstill and not reverse) or self.total_engagement_time == 0) else 0
curr_steer = int(abs(car_state.steeringAngleDeg)) if car_state else 0
curr_torque = int(abs(car_control.actuators.torque * 100)) if (car_control and hasattr(car_control.actuators, 'torque')) else 0
now = time.monotonic()
if curr_torque >= 50:
self.max_steer_angle = max(self.max_steer_angle, curr_steer)
self.max_torque = max(self.max_torque, curr_torque)
self.torque_timer_start = now
elif self.torque_timer_start > 0.0 and (now - self.torque_timer_start >= 10.0):
self.max_torque = 0
self.max_steer_angle = 0
self.torque_timer_start = 0.0
steer_label = f"{curr_steer}°"
torque_label = f"{curr_torque}%"
if curr_torque >= 50 or self.torque_timer_start > 0.0:
steer_label += f" - ({self.max_steer_angle}°)"
torque_label += f" - ({self.max_torque}%)"
force_auto_tune_off = ui_state.starpilot_toggles.get("force_auto_tune_off", False) or self._cached_force_auto_tune_off
force_auto_tune = ui_state.starpilot_toggles.get("force_auto_tune", False) or self._cached_force_auto_tune
use_params = live_torque_parameters.useParams if (live_torque_parameters and hasattr(live_torque_parameters, 'useParams')) else False
using_live_torque = not force_auto_tune_off and (use_params or force_auto_tune)
if not using_live_torque:
friction_coeff = self._cached_friction_stock
else:
friction_coeff = live_torque_parameters.frictionCoefficientFiltered if (live_torque_parameters and hasattr(live_torque_parameters, 'frictionCoefficientFiltered')) else 0.0
if friction_coeff == 0.0:
friction_coeff = self._cached_friction if force_auto_tune_off else (live_torque_parameters.frictionCoefficientFiltered if (live_torque_parameters and hasattr(live_torque_parameters, 'frictionCoefficientFiltered')) else 0.0)
if not using_live_torque:
lat_factor = self._cached_lat_stock
else:
lat_factor = live_torque_parameters.latAccelFactorFiltered if (live_torque_parameters and hasattr(live_torque_parameters, 'latAccelFactorFiltered')) else 0.0
if lat_factor == 0.0:
lat_factor = self._cached_lat if force_auto_tune_off else (live_torque_parameters.latAccelFactorFiltered if (live_torque_parameters and hasattr(live_torque_parameters, 'latAccelFactorFiltered')) else 0.0)
lat_delay = live_delay.lateralDelay if live_delay else 0.0
tot_time = max(1, self.total_engagement_time)
lat_pct = (self.lateral_engagement_time / tot_time) * 100.0
long_pct = (self.longitudinal_engagement_time / tot_time) * 100.0
accel_jerk = starpilot_plan.accelerationJerk if starpilot_plan else 0.0
act_accel = (car_control.actuators.accel if (car_control and hasattr(car_control.actuators, 'accel')) else 0.0) * accel_conv
danger_factor = (starpilot_plan.dangerFactor if starpilot_plan else 0.0) * 100.0
danger_jerk = starpilot_plan.dangerJerk if starpilot_plan else 0.0
speed_jerk = starpilot_plan.speedJerk if starpilot_plan else 0.0
steer_ratio = live_parameters.steerRatio if live_parameters else 0.0
stiff_factor = live_parameters.stiffnessFactor if live_parameters else 0.0
model_name = ui_state.starpilot_toggles.get("model_name", "N/A")
model_name = re.sub(r'\(.*\)', '', model_name)
model_name = re.sub(r'[^a-zA-Z0-9 \-\.:]', '', model_name).strip()
self._metrics = {
1: ("ACCEL", f"{accel_val:.2f}{accel_unit}"),
2: ("MAX ACCEL", f"{self.max_acceleration:.2f}{accel_unit}"),
3: ("STEER DELAY", f"{lat_delay:.5f}"),
4: ("FRICTION", f"{friction_coeff:.5f}"),
5: ("LAT ACCEL", f"{lat_factor:.5f}"),
6: ("STEER RATIO", f"{steer_ratio:.5f}"),
7: ("STEER STIFF", f"{stiff_factor:.5f}"),
8: ("LATERAL %", f"{lat_pct:.2f}%"),
9: ("LONG %", f"{long_pct:.2f}%"),
10: ("STEER ANGLE", steer_label),
11: ("TORQUE %", torque_label),
12: ("ACT ACCEL", f"{act_accel:.2f}{accel_unit}"),
13: ("DANGER %", f"{danger_factor:.2f}%"),
14: ("ACCEL JERK", f"{accel_jerk}"),
15: ("DANGER JERK", f"{danger_jerk}"),
16: ("SPEED JERK", f"{speed_jerk}"),
17: (model_name, "")
}
def render(self, sidebar_rect: rl.Rectangle):
if not self._visible:
return
count = len(self._active_ids)
if count == 0:
return
rl.draw_rectangle_rec(sidebar_rect, rl.BLACK)
spacing = max(1, (int(sidebar_rect.height) - (count * METRIC_HEIGHT)) // max(1, (count + 1)))
y = sidebar_rect.y + spacing
for metric_id in self._active_ids:
if metric_id <= 0 or metric_id not in self._metrics:
continue
label_first, label_second = self._metrics[metric_id]
self._draw_metric(sidebar_rect, label_first, label_second, self._metric_color, y)
y += METRIC_HEIGHT + spacing