Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025

This commit is contained in:
Jason Wen
2026-02-10 23:41:27 -05:00
5 changed files with 220 additions and 19 deletions
+133 -12
View File
@@ -4,27 +4,148 @@ 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.
"""
import requests
import threading
import time
import pyray as rl
from openpilot.common.api import api_get
from openpilot.common.constants import CV
from openpilot.common.params import Params
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.ui.lib.api_helpers import get_token
from openpilot.selfdrive.ui.ui_state import ui_state, device
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.widgets import Widget
class TripsLayout(Widget):
PARAM_KEY = "ApiCache_DriveStats"
UPDATE_INTERVAL = 30 # seconds
def __init__(self):
super().__init__()
self._params = Params()
items = self._initialize_items()
self._scroller = Scroller(items, line_separator=True, spacing=0)
self._session = requests.Session()
self._stats = self._get_stats()
def _initialize_items(self):
items = [
self._icon_distance = gui_app.texture("icons/road.png", 100, 100, keep_aspect_ratio=True)
self._icon_drives = gui_app.texture("icons_mici/wheel.png", 80, 80, keep_aspect_ratio=True)
self._icon_hours = gui_app.texture("../../sunnypilot/selfdrive/assets/icons/clock.png", 80, 80, keep_aspect_ratio=True)
]
return items
self._running = True
self._update_thread = threading.Thread(target=self._update_loop, daemon=True)
self._update_thread.start()
def _render(self, rect):
self._scroller.render(rect)
def __del__(self):
self._running = False
try:
if self._update_thread and self._update_thread.is_alive():
self._update_thread.join(timeout=1.0)
except Exception:
pass
def show_event(self):
self._scroller.show_event()
def _get_stats(self):
stats = self._params.get(self.PARAM_KEY)
if not stats:
return {}
try:
return stats
except Exception:
cloudlog.exception(f"Failed to decode drive stats: {stats}")
return {}
def _fetch_drive_stats(self):
try:
dongle_id = self._params.get("DongleId")
if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID:
return
identity_token = get_token(dongle_id)
response = api_get(f"v1.1/devices/{dongle_id}/stats", access_token=identity_token, session=self._session)
if response.status_code == 200:
data = response.json()
self._stats = data
self._params.put(self.PARAM_KEY, data)
except Exception as e:
cloudlog.error(f"Failed to fetch drive stats: {e}")
def _update_loop(self):
while self._running:
if not ui_state.started and device._awake:
self._fetch_drive_stats()
time.sleep(self.UPDATE_INTERVAL)
def _render_stat_group(self, x, y, width, height, title, data, is_metric):
# Card Background
rl.draw_rectangle_rounded(rl.Rectangle(x, y, width, height), 0.05, 10, rl.Color(30, 30, 30, 255))
# Title
title_font = gui_app.font(FontWeight.BOLD)
rl.draw_text_ex(title_font, title, rl.Vector2(x + 60, y + 30), 50 * FONT_SCALE, 0, rl.Color(200, 200, 200, 255))
# Internal content area
# Center the content block (Icon + Value + Unit) vertically
content_y = y + (height / 2) - (140 * FONT_SCALE)
col_width = width / 3
# Values
number_font = gui_app.font(FontWeight.BOLD)
unit_font = gui_app.font(FontWeight.LIGHT)
number_base_size = 92
unit_base_size = 55
number_size = number_base_size * FONT_SCALE
unit_size = unit_base_size * FONT_SCALE
color_unit = rl.Color(160, 160, 160, 255)
routes = int(data.get("routes", 0))
distance = data.get("distance", 0)
distance_str = str(int(distance * CV.MPH_TO_KPH)) if is_metric else str(int(distance))
hours = int(data.get("minutes", 0) / 60)
dist_unit = tr("KM") if is_metric else tr("Miles")
def draw_col(col_idx, icon, value, unit):
col_x = x + (col_width * col_idx)
center_x = col_x + (col_width / 2)
# Icon
icon_x = int(center_x - (icon.width / 2))
icon_y = int(content_y + 60)
rl.draw_texture(icon, icon_x, icon_y, rl.WHITE)
# Value
val_size = measure_text_cached(number_font, value, number_base_size)
rl.draw_text_ex(number_font, value, rl.Vector2(center_x - val_size.x / 1.65, content_y + 145 * FONT_SCALE), number_size, 0, rl.WHITE)
# Unit
unit_size_vec = measure_text_cached(unit_font, unit, unit_base_size)
rl.draw_text_ex(unit_font, unit, rl.Vector2(center_x - unit_size_vec.x / 1.65, content_y + 255 * FONT_SCALE), unit_size, 0, color_unit)
draw_col(0, self._icon_drives, str(routes), tr("Drives"))
draw_col(1, self._icon_distance, distance_str, dist_unit)
draw_col(2, self._icon_hours, str(hours), tr("Hours"))
return y + height
def _render(self, rect: rl.Rectangle):
x = rect.x
y = rect.y
w = rect.width
spacing = 30
available_h = rect.height - 30
card_height = available_h / 2
is_metric = self._params.get_bool("IsMetric")
all_time = self._stats.get("all", {})
week = self._stats.get("week", {})
y = self._render_stat_group(x, y, w, card_height, tr("ALL TIME"), all_time, is_metric)
y += spacing
y = self._render_stat_group(x, y, w, card_height, tr("PAST WEEK"), week, is_metric)
return -1
@@ -19,8 +19,8 @@ from openpilot.system.ui.widgets import Widget
class DeveloperUiRenderer(Widget):
DEV_UI_OFF = 0
DEV_UI_RIGHT = 1
DEV_UI_BOTTOM = 2
DEV_UI_BOTTOM = 1
DEV_UI_RIGHT = 2
DEV_UI_BOTH = 3
BOTTOM_BAR_HEIGHT = 61
@@ -62,10 +62,10 @@ class DeveloperUiRenderer(Widget):
if sm.recv_frame["carState"] < ui_state.started_frame:
return
if self.dev_ui_mode == self.DEV_UI_RIGHT:
self._draw_right_dev_ui(rect)
elif self.dev_ui_mode == self.DEV_UI_BOTTOM:
if self.dev_ui_mode == self.DEV_UI_BOTTOM:
self._draw_bottom_dev_ui(rect)
elif self.dev_ui_mode == self.DEV_UI_RIGHT:
self._draw_right_dev_ui(rect)
elif self.dev_ui_mode == self.DEV_UI_BOTH:
self._draw_right_dev_ui(rect)
self._draw_bottom_dev_ui(rect)
+78 -2
View File
@@ -6,9 +6,8 @@ See the LICENSE.md file in the root directory for more details.
"""
import pyray as rl
from openpilot.common.constants import CV
from openpilot.selfdrive.ui.mici.onroad.torque_bar import TorqueBar
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.onroad.hud_renderer import HudRenderer
from openpilot.selfdrive.ui.sunnypilot.onroad.developer_ui import DeveloperUiRenderer
from openpilot.selfdrive.ui.sunnypilot.onroad.road_name import RoadNameRenderer
from openpilot.selfdrive.ui.sunnypilot.onroad.rocket_fuel import RocketFuel
@@ -17,6 +16,11 @@ from openpilot.selfdrive.ui.sunnypilot.onroad.smart_cruise_control import SmartC
from openpilot.selfdrive.ui.sunnypilot.onroad.turn_signal import TurnSignalController
from openpilot.selfdrive.ui.sunnypilot.onroad.circular_alerts import CircularAlertsRenderer
from openpilot.selfdrive.ui.sunnypilot.onroad.speed_renderer import SpeedRenderer
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
from openpilot.selfdrive.ui.onroad.hud_renderer import HudRenderer, UI_CONFIG, FONT_SIZES, COLORS, CRUISE_DISABLED_CHAR
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.text_measure import measure_text_cached
class HudRendererSP(HudRenderer):
@@ -32,7 +36,21 @@ class HudRendererSP(HudRenderer):
self.speed_renderer = SpeedRenderer()
self._torque_bar = TorqueBar(scale=3.0, always=True)
self.pcm_cruise_speed: bool = True
self.show_icbm_status: bool = False
self.icbm_active_counter: int = 0
self.speed_cluster: float = 0.0
self.speed_conv: float = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
def _update_state(self) -> None:
if ui_state.sm.recv_frame["carState"] < ui_state.started_frame:
return
if ui_state.CP_SP is not None:
self.pcm_cruise_speed = ui_state.CP_SP.pcmCruiseSpeed
self.speed_conv = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
self.speed_cluster = ui_state.sm['carState'].cruiseState.speedCluster * self.speed_conv
super()._update_state()
self.road_name_renderer.update()
self.speed_limit_renderer.update()
@@ -41,6 +59,64 @@ class HudRendererSP(HudRenderer):
self.circular_alerts_renderer.update()
self.speed_renderer.update()
def _get_icbm_status(self):
if not self.pcm_cruise_speed and ui_state.sm['carControl'].enabled:
if round(self.set_speed) != round(self.speed_cluster):
self.icbm_active_counter = 3 * gui_app.target_fps # 3 seconds usually
elif self.icbm_active_counter > 0:
self.icbm_active_counter -= 1
else:
self.icbm_active_counter = 0
self.show_icbm_status = self.icbm_active_counter > 0
def _draw_set_speed(self, rect: rl.Rectangle) -> None:
self._get_icbm_status()
set_speed_width = UI_CONFIG.set_speed_width_metric if ui_state.is_metric else UI_CONFIG.set_speed_width_imperial
x = rect.x + 60 + (UI_CONFIG.set_speed_width_imperial - set_speed_width) // 2
y = rect.y + 45
set_speed_rect = rl.Rectangle(x, y, set_speed_width, UI_CONFIG.set_speed_height)
rl.draw_rectangle_rounded(set_speed_rect, 0.35, 10, COLORS.BLACK_TRANSLUCENT)
rl.draw_rectangle_rounded_lines_ex(set_speed_rect, 0.35, 10, 6, COLORS.BORDER_TRANSLUCENT)
max_color = COLORS.GREY
set_speed_color = COLORS.DARK_GREY
if self.is_cruise_set:
set_speed_color = COLORS.WHITE
if ui_state.status == UIStatus.ENGAGED:
max_color = COLORS.ENGAGED
elif ui_state.status == UIStatus.DISENGAGED:
max_color = COLORS.DISENGAGED
elif ui_state.status == UIStatus.OVERRIDE:
max_color = COLORS.OVERRIDE
max_str_size = 60 if self.show_icbm_status else 40
max_str_y = 15 if self.show_icbm_status else 27
max_text = str(round(self.speed_cluster)) if self.show_icbm_status else tr("MAX")
max_text_width = measure_text_cached(self._font_semi_bold, max_text, max_str_size).x
rl.draw_text_ex(
self._font_semi_bold,
max_text,
rl.Vector2(x + (set_speed_width - max_text_width) / 2, y + max_str_y),
max_str_size,
0,
max_color,
)
set_speed_text = CRUISE_DISABLED_CHAR if not self.is_cruise_set else str(round(self.set_speed))
speed_text_width = measure_text_cached(self._font_bold, set_speed_text, FONT_SIZES.set_speed).x
rl.draw_text_ex(
self._font_bold,
set_speed_text,
rl.Vector2(x + (set_speed_width - speed_text_width) / 2, y + 77),
FONT_SIZES.set_speed,
0,
set_speed_color,
)
def _draw_current_speed(self, rect: rl.Rectangle) -> None:
self.speed_renderer.render(rect)
+1
View File
@@ -26,6 +26,7 @@ class OnroadTimerStatus(Enum):
class UIStateSP:
def __init__(self):
self.CP_SP: custom.CarParamsSP | None = None
self.params = Params()
self.sm_services_ext = [
"modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP",
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e095cfc4de71788bd4a99699a2e7ab4098cd426277d672b9e43981c5fab8b40f
size 16407