mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-08-05 16:26:06 +08:00
Big UI : Stopped Timer Fix
This commit is contained in:
@@ -316,8 +316,7 @@ class StarPilotAppearanceLayout(_SettingsPage):
|
||||
SettingRow("StoppedTimer", "toggle", tr_noop("Stopped Timer"),
|
||||
subtitle="",
|
||||
get_state=lambda: self._params.get_bool("StoppedTimer"),
|
||||
set_state=lambda s: self._params.put_bool("StoppedTimer", s),
|
||||
visible=hud_on),
|
||||
set_state=lambda s: self._params.put_bool("StoppedTimer", s)),
|
||||
SettingRow("ShowCSCStatus", "toggle", tr_noop("CSC Status Widget"),
|
||||
subtitle=tr_noop("Show the Curve Speed Controller target speed and ambient border glow."),
|
||||
get_state=lambda: self._params.get_bool("ShowCSCStatus"),
|
||||
|
||||
@@ -76,6 +76,7 @@ class HudRenderer(Widget):
|
||||
self._navigation_card = NavigationCardRenderer()
|
||||
|
||||
self.draw_set_speed = True
|
||||
self.draw_current_speed = True
|
||||
self.draw_exp_button = True
|
||||
|
||||
def _update_state(self) -> None:
|
||||
@@ -136,7 +137,7 @@ class HudRenderer(Widget):
|
||||
if self.draw_set_speed and self.is_cruise_available and not ui_state.starpilot_toggles.get("hide_max_speed", False):
|
||||
self._draw_set_speed(rect)
|
||||
|
||||
if not ui_state.starpilot_toggles.get("hide_speed", False):
|
||||
if self.draw_current_speed and not ui_state.starpilot_toggles.get("hide_speed", False):
|
||||
self._draw_current_speed(rect)
|
||||
|
||||
self._navigation_card.render(rect)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import pyray as rl
|
||||
import time
|
||||
from msgq.visionipc import VisionStreamType
|
||||
from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView
|
||||
from openpilot.selfdrive.ui.onroad.starpilot.starpilot_border import render_behind, render_overlay, render_background_effects
|
||||
@@ -10,14 +9,13 @@ from openpilot.selfdrive.ui.onroad.starpilot.widget_layout_manager import Widget
|
||||
from openpilot.selfdrive.ui.onroad.starpilot.widgets import (
|
||||
SetSpeedWidget, SpeedLimitWidget, PedalIconsWidget,
|
||||
AetherGaugeWidget, PersonalityButtonWidget, DriverMonitorWidget,
|
||||
SteeringWheelWidget
|
||||
SteeringWheelWidget, StoppedTimerWidget
|
||||
)
|
||||
from openpilot.selfdrive.ui.onroad.starpilot.stopping_point import render_stopping_point
|
||||
from openpilot.selfdrive.ui.onroad.starpilot.pause_indicators import render_lateral_paused, render_longitudinal_paused
|
||||
from openpilot.selfdrive.ui.onroad.starpilot.weather_icon import render_weather_icon
|
||||
from openpilot.selfdrive.ui.lib.starpilot_status import (
|
||||
get_screen_edge_color, ENGAGED_COLOR,
|
||||
EXPERIMENTAL_COLOR, TRAFFIC_COLOR,
|
||||
get_screen_edge_color,
|
||||
)
|
||||
|
||||
from openpilot.system.ui.lib.application import MousePos, gui_app, FontWeight
|
||||
@@ -34,7 +32,6 @@ class StarPilotOnroadView(AugmentedRoadView):
|
||||
|
||||
self._font_bold = gui_app.font(FontWeight.BOLD)
|
||||
self._font_medium = gui_app.font(FontWeight.MEDIUM)
|
||||
self._standstill_started_at = 0.0
|
||||
self._torque_bar = TorqueBar()
|
||||
self._min_fps = 99.9
|
||||
self._max_fps = 0.0
|
||||
@@ -55,6 +52,7 @@ class StarPilotOnroadView(AugmentedRoadView):
|
||||
self._pedals_widget = PedalIconsWidget()
|
||||
self._personality_button_widget = PersonalityButtonWidget()
|
||||
self._driver_monitor_widget = DriverMonitorWidget(self.driver_state_renderer)
|
||||
self._stopped_timer_widget = StoppedTimerWidget(self.is_in_reverse)
|
||||
|
||||
# Register to layout zones
|
||||
self.layout_manager.register_widget("left", self._set_speed_widget)
|
||||
@@ -73,12 +71,17 @@ class StarPilotOnroadView(AugmentedRoadView):
|
||||
self._child(self._pedals_widget)
|
||||
self._child(self._personality_button_widget)
|
||||
self._child(self._driver_monitor_widget)
|
||||
self._child(self._stopped_timer_widget)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
border_width = self._get_border_width()
|
||||
border_color = get_screen_edge_color(ui_state)
|
||||
rl.draw_rectangle_rounded(rect, 0.12, 10, border_color)
|
||||
render_background_effects(rect, border_width)
|
||||
|
||||
self._hud_renderer.draw_current_speed = (
|
||||
ui_state.started and not self._stopped_timer_widget.replaces_current_speed
|
||||
)
|
||||
super()._render(rect)
|
||||
|
||||
if not ui_state.started:
|
||||
@@ -109,10 +112,13 @@ class StarPilotOnroadView(AugmentedRoadView):
|
||||
|
||||
def _render_overlays(self):
|
||||
alert_showing, _ = self.alert_renderer.will_render()
|
||||
if alert_showing is not None and alert_showing.size == AlertSize.full:
|
||||
return
|
||||
|
||||
self._stopped_timer_widget.render(self._content_rect)
|
||||
if alert_showing is not None:
|
||||
return
|
||||
|
||||
self._render_standstill_timer()
|
||||
self._render_developer_metrics()
|
||||
|
||||
self.layout_manager.render_widgets(exclude={"speed_limit", "set_speed"})
|
||||
@@ -155,71 +161,6 @@ class StarPilotOnroadView(AugmentedRoadView):
|
||||
alert_showing, _ = self.alert_renderer.will_render()
|
||||
return alert_showing is not None and alert_showing.size == AlertSize.full
|
||||
|
||||
def _render_standstill_timer(self):
|
||||
if not self._params.get_bool("stopped_timer"):
|
||||
self._standstill_started_at = 0.0
|
||||
return
|
||||
if not ui_state.sm.valid.get("carState", False):
|
||||
return
|
||||
|
||||
car_state = ui_state.sm["carState"]
|
||||
if getattr(car_state, "standstill", False):
|
||||
if self._standstill_started_at == 0.0:
|
||||
self._standstill_started_at = time.monotonic()
|
||||
else:
|
||||
self._standstill_started_at = 0.0
|
||||
return
|
||||
|
||||
if self._standstill_started_at == 0.0:
|
||||
return
|
||||
|
||||
duration = int(time.monotonic() - self._standstill_started_at)
|
||||
if duration < 60:
|
||||
return
|
||||
|
||||
minutes = duration // 60
|
||||
seconds = duration % 60
|
||||
minute_text = f"{minutes} minute{'s' if minutes != 1 else ''}"
|
||||
second_text = f"{seconds} second{'s' if seconds != 1 else ''}"
|
||||
minute_size = measure_text_cached(self._font_bold, minute_text, 176)
|
||||
second_size = measure_text_cached(self._font_medium, second_text, 66)
|
||||
|
||||
def blend_colors(start: rl.Color, end: rl.Color, transition: float) -> rl.Color:
|
||||
transition = float(min(max(transition, 0.0), 1.0))
|
||||
return rl.Color(
|
||||
int(start.r + transition * (end.r - start.r)),
|
||||
int(start.g + transition * (end.g - start.g)),
|
||||
int(start.b + transition * (end.b - start.b)),
|
||||
255,
|
||||
)
|
||||
|
||||
if duration < 150:
|
||||
transition = (duration - 60) / 90.0
|
||||
duration_color = blend_colors(ENGAGED_COLOR, EXPERIMENTAL_COLOR, transition)
|
||||
elif duration < 300:
|
||||
transition = (duration - 150) / 150.0
|
||||
duration_color = blend_colors(EXPERIMENTAL_COLOR, TRAFFIC_COLOR, transition)
|
||||
else:
|
||||
duration_color = TRAFFIC_COLOR
|
||||
|
||||
x = gui_app.width / 2
|
||||
rl.draw_text_ex(
|
||||
self._font_bold,
|
||||
minute_text,
|
||||
rl.Vector2(x - minute_size.x / 2, 210 - minute_size.y / 2),
|
||||
176,
|
||||
0,
|
||||
duration_color,
|
||||
)
|
||||
rl.draw_text_ex(
|
||||
self._font_medium,
|
||||
second_text,
|
||||
rl.Vector2(x - second_size.x / 2, 290 - second_size.y / 2),
|
||||
66,
|
||||
0,
|
||||
rl.Color(255, 255, 255, 242),
|
||||
)
|
||||
|
||||
def _handle_mouse_press(self, mouse_pos: MousePos):
|
||||
# Check if click maps to any of the layout widgets
|
||||
for zone in self.layout_manager.zones.values():
|
||||
|
||||
@@ -76,4 +76,3 @@ class WidgetLayoutManager:
|
||||
for widget in zone:
|
||||
if widget.is_visible and widget.name not in skip:
|
||||
widget.render(widget.rect)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from openpilot.selfdrive.ui.onroad.starpilot.widgets.aethergauge import AetherGa
|
||||
from openpilot.selfdrive.ui.onroad.starpilot.widgets.personality_button import PersonalityButtonWidget
|
||||
from openpilot.selfdrive.ui.onroad.starpilot.widgets.driver_monitor import DriverMonitorWidget
|
||||
from openpilot.selfdrive.ui.onroad.starpilot.widgets.steering_wheel import SteeringWheelWidget
|
||||
from openpilot.selfdrive.ui.onroad.starpilot.widgets.stopped_timer import StoppedTimerWidget
|
||||
|
||||
__all__ = [
|
||||
"LayoutWidget",
|
||||
@@ -16,4 +17,5 @@ __all__ = [
|
||||
"PersonalityButtonWidget",
|
||||
"DriverMonitorWidget",
|
||||
"SteeringWheelWidget",
|
||||
"StoppedTimerWidget",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.lib.starpilot_status import (
|
||||
ENGAGED_COLOR, EXPERIMENTAL_COLOR, TRAFFIC_COLOR,
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
class StoppedTimerWidget(Widget):
|
||||
SHOW_AFTER_SECONDS = 60
|
||||
|
||||
def __init__(self, in_reverse: Callable[[], bool] | None = None):
|
||||
super().__init__()
|
||||
self.set_enabled(False)
|
||||
self._in_reverse = in_reverse or (lambda: False)
|
||||
self._font_bold = gui_app.font(FontWeight.BOLD)
|
||||
self._font_normal = gui_app.font(FontWeight.NORMAL)
|
||||
self._standstill_started_at: float | None = None
|
||||
self._started_frame = -1
|
||||
self._duration = 0
|
||||
|
||||
@property
|
||||
def is_visible(self) -> bool:
|
||||
return self._update_timer() > 0
|
||||
|
||||
@property
|
||||
def replaces_current_speed(self) -> bool:
|
||||
return self.is_visible
|
||||
|
||||
def _update_timer(self) -> int:
|
||||
started_frame = getattr(ui_state, "started_frame", 0)
|
||||
if started_frame != self._started_frame:
|
||||
self._started_frame = started_frame
|
||||
self._reset_timer()
|
||||
|
||||
params = ui_state.ui_params
|
||||
if (not ui_state.started or
|
||||
not params.get_bool("QOLVisuals") or
|
||||
not params.get_bool("StoppedTimer") or
|
||||
not ui_state.sm.valid.get("carState", False)):
|
||||
self._reset_timer()
|
||||
return 0
|
||||
|
||||
if self._in_reverse():
|
||||
self._reset_timer()
|
||||
return 0
|
||||
|
||||
try:
|
||||
if ui_state.sm.recv_frame["carState"] < started_frame:
|
||||
self._reset_timer()
|
||||
return 0
|
||||
except (AttributeError, KeyError, TypeError):
|
||||
pass
|
||||
|
||||
if not getattr(ui_state.sm["carState"], "standstill", False):
|
||||
self._reset_timer()
|
||||
return 0
|
||||
|
||||
now = time.monotonic()
|
||||
if self._standstill_started_at is None:
|
||||
self._standstill_started_at = now
|
||||
|
||||
if now - getattr(ui_state, "started_time", 0.0) < self.SHOW_AFTER_SECONDS:
|
||||
self._duration = 0
|
||||
return 0
|
||||
|
||||
self._duration = max(0, int(now - self._standstill_started_at))
|
||||
return self._duration
|
||||
|
||||
def _reset_timer(self) -> None:
|
||||
self._standstill_started_at = None
|
||||
self._duration = 0
|
||||
|
||||
@staticmethod
|
||||
def _format_duration_text(duration: int) -> tuple[str, str]:
|
||||
minutes = duration // 60
|
||||
seconds = duration % 60
|
||||
return (
|
||||
f"{minutes} minute{'s' if minutes != 1 else ''}",
|
||||
f"{seconds} second{'s' if seconds != 1 else ''}",
|
||||
)
|
||||
|
||||
def _render(self, rect: rl.Rectangle) -> None:
|
||||
duration = self._duration
|
||||
minute_text, second_text = self._format_duration_text(duration)
|
||||
|
||||
minute_size = measure_text_cached(self._font_bold, minute_text, 176)
|
||||
second_size = measure_text_cached(self._font_normal, second_text, 66)
|
||||
|
||||
if duration < 150:
|
||||
transition = (duration - 60) / 90.0
|
||||
duration_color = self._blend_colors(ENGAGED_COLOR, EXPERIMENTAL_COLOR, transition)
|
||||
elif duration < 300:
|
||||
transition = (duration - 150) / 150.0
|
||||
duration_color = self._blend_colors(EXPERIMENTAL_COLOR, TRAFFIC_COLOR, transition)
|
||||
else:
|
||||
duration_color = TRAFFIC_COLOR
|
||||
|
||||
center_x = rect.x + rect.width / 2
|
||||
rl.draw_text_ex(
|
||||
self._font_bold,
|
||||
minute_text,
|
||||
rl.Vector2(center_x - minute_size.x / 2, rect.y + 210 - minute_size.y),
|
||||
176,
|
||||
0,
|
||||
duration_color,
|
||||
)
|
||||
rl.draw_text_ex(
|
||||
self._font_normal,
|
||||
second_text,
|
||||
rl.Vector2(center_x - second_size.x / 2, rect.y + 290 - second_size.y),
|
||||
66,
|
||||
0,
|
||||
rl.Color(255, 255, 255, 255),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _blend_colors(start: rl.Color, end: rl.Color, transition: float) -> rl.Color:
|
||||
transition = min(max(transition, 0.0), 1.0)
|
||||
return rl.Color(
|
||||
int(start.r + transition * (end.r - start.r)),
|
||||
int(start.g + transition * (end.g - start.g)),
|
||||
int(start.b + transition * (end.b - start.b)),
|
||||
255,
|
||||
)
|
||||
@@ -75,6 +75,7 @@ def _load_starpilot_onroad_view(monkeypatch):
|
||||
PersonalityButtonWidget=dummy_widget,
|
||||
DriverMonitorWidget=dummy_widget,
|
||||
SteeringWheelWidget=dummy_widget,
|
||||
StoppedTimerWidget=dummy_widget,
|
||||
)
|
||||
stub_module(
|
||||
"openpilot.selfdrive.ui.onroad.starpilot.stopping_point",
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import ModuleType, SimpleNamespace
|
||||
|
||||
|
||||
def _load_stopped_timer(monkeypatch):
|
||||
rl = SimpleNamespace(
|
||||
Color=lambda r, g, b, a=255: SimpleNamespace(r=r, g=g, b=b, a=a),
|
||||
Rectangle=lambda x=0, y=0, width=0, height=0: SimpleNamespace(x=x, y=y, width=width, height=height),
|
||||
Vector2=lambda x, y: SimpleNamespace(x=x, y=y),
|
||||
WHITE=SimpleNamespace(r=255, g=255, b=255, a=255),
|
||||
draw_text_ex=lambda *_args: None,
|
||||
)
|
||||
monkeypatch.setitem(sys.modules, "pyray", rl)
|
||||
|
||||
def module(name, **attributes):
|
||||
result = ModuleType(name)
|
||||
for key, value in attributes.items():
|
||||
setattr(result, key, value)
|
||||
monkeypatch.setitem(sys.modules, name, result)
|
||||
|
||||
class Widget:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def set_enabled(self, _enabled):
|
||||
pass
|
||||
|
||||
module("openpilot.system.ui.widgets", Widget=Widget)
|
||||
module(
|
||||
"openpilot.system.ui.lib.application",
|
||||
FontWeight=SimpleNamespace(BOLD=1, NORMAL=2),
|
||||
gui_app=SimpleNamespace(font=lambda *_args: None),
|
||||
)
|
||||
module(
|
||||
"openpilot.system.ui.lib.text_measure",
|
||||
measure_text_cached=lambda *_args: SimpleNamespace(x=100, y=20),
|
||||
)
|
||||
module(
|
||||
"openpilot.selfdrive.ui.lib.starpilot_status",
|
||||
ENGAGED_COLOR=rl.Color(22, 127, 64),
|
||||
EXPERIMENTAL_COLOR=rl.Color(218, 111, 37),
|
||||
TRAFFIC_COLOR=rl.Color(201, 34, 49),
|
||||
)
|
||||
module("openpilot.selfdrive.ui.ui_state", ui_state=SimpleNamespace())
|
||||
|
||||
module_path = Path(__file__).parents[1] / "onroad/starpilot/widgets/stopped_timer.py"
|
||||
spec = importlib.util.spec_from_file_location("stopped_timer_under_test", module_path)
|
||||
stopped_timer = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(stopped_timer)
|
||||
return stopped_timer
|
||||
|
||||
|
||||
def test_stopped_timer_visibility_waits_for_onroad_grace_period(monkeypatch):
|
||||
stopped_timer = _load_stopped_timer(monkeypatch)
|
||||
params = SimpleNamespace(get_bool=lambda key: key in {"QOLVisuals", "StoppedTimer"})
|
||||
car_state = SimpleNamespace(standstill=True)
|
||||
|
||||
class SubMaster:
|
||||
valid = {"carState": True}
|
||||
recv_frame = {"carState": 1}
|
||||
|
||||
def __getitem__(self, _key):
|
||||
return car_state
|
||||
|
||||
ui_state = SimpleNamespace(
|
||||
started=True,
|
||||
started_frame=1,
|
||||
started_time=100.0,
|
||||
ui_params=params,
|
||||
sm=SubMaster(),
|
||||
)
|
||||
monkeypatch.setattr(stopped_timer, "ui_state", ui_state)
|
||||
|
||||
now = iter((100.0, 159.9, 160.0))
|
||||
monkeypatch.setattr(stopped_timer.time, "monotonic", lambda: next(now))
|
||||
|
||||
widget = stopped_timer.StoppedTimerWidget()
|
||||
|
||||
assert not widget.is_visible
|
||||
assert not widget.is_visible
|
||||
assert widget.is_visible
|
||||
|
||||
|
||||
def test_stopped_timer_uses_qt_text_contract(monkeypatch):
|
||||
stopped_timer = _load_stopped_timer(monkeypatch)
|
||||
|
||||
assert stopped_timer.StoppedTimerWidget._format_duration_text(61) == ("1 minute", "1 second")
|
||||
assert stopped_timer.StoppedTimerWidget._format_duration_text(121) == ("2 minutes", "1 second")
|
||||
|
||||
|
||||
def test_stopped_timer_draws_qt_positions_and_opaque_seconds(monkeypatch):
|
||||
stopped_timer = _load_stopped_timer(monkeypatch)
|
||||
widget = stopped_timer.StoppedTimerWidget()
|
||||
widget._duration = 61
|
||||
draws = []
|
||||
monkeypatch.setattr(stopped_timer.rl, "draw_text_ex", lambda *args: draws.append(args))
|
||||
|
||||
widget._render(stopped_timer.rl.Rectangle(0, 0, 2160, 1080))
|
||||
|
||||
assert draws[0][2].x == 1030
|
||||
assert draws[0][2].y == 190
|
||||
assert draws[0][3] == 176
|
||||
assert draws[1][2].x == 1030
|
||||
assert draws[1][2].y == 270
|
||||
assert draws[1][3] == 66
|
||||
assert draws[1][5].a == 255
|
||||
Reference in New Issue
Block a user