mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-06-28 00:42:06 +08:00
Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user