mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-30 19:33:45 +08:00
ui: add experimental mode toggle button with visual indicator (#35446)
* add experimental mode toggle button with visual indicator * merge master * implement a temporary state hold after mouse click" * move to seperate class --------- Co-authored-by: Shane Smiskol <shane@smiskol.com>
This commit is contained in:
@@ -38,7 +38,6 @@ class MainLayout(Widget):
|
||||
self._update_layout_rects(rect)
|
||||
self._handle_onroad_transition()
|
||||
self._render_main_content()
|
||||
self._handle_input()
|
||||
|
||||
if self._current_callback:
|
||||
self._current_callback()
|
||||
@@ -51,6 +50,7 @@ class MainLayout(Widget):
|
||||
self._layouts[MainState.SETTINGS].set_callbacks(
|
||||
on_close=lambda: setattr(self, '_current_callback', self._set_mode_for_state)
|
||||
)
|
||||
self._layouts[MainState.ONROAD].on_click = self._on_onrad_clicked
|
||||
|
||||
def _update_layout_rects(self, rect):
|
||||
self._window_rect = rect
|
||||
@@ -80,6 +80,9 @@ class MainLayout(Widget):
|
||||
def _on_flag_clicked(self):
|
||||
pass
|
||||
|
||||
def _on_onrad_clicked(self):
|
||||
self._sidebar_visible = not self._sidebar_visible
|
||||
|
||||
def _render_main_content(self):
|
||||
# Render sidebar
|
||||
if self._sidebar_visible:
|
||||
@@ -87,11 +90,3 @@ class MainLayout(Widget):
|
||||
|
||||
content_rect = self._content_rect if self._sidebar_visible else self._window_rect
|
||||
self._layouts[self._current_mode].render(content_rect)
|
||||
|
||||
def _handle_input(self):
|
||||
if self._current_mode != MainState.ONROAD or not rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
||||
return
|
||||
|
||||
mouse_pos = rl.get_mouse_position()
|
||||
if rl.check_collision_point_rec(mouse_pos, self._content_rect):
|
||||
self._sidebar_visible = not self._sidebar_visible
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import numpy as np
|
||||
import pyray as rl
|
||||
|
||||
from collections.abc import Callable
|
||||
from cereal import log
|
||||
from msgq.visionipc import VisionStreamType
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus, UI_BORDER_SIZE
|
||||
@@ -49,6 +49,9 @@ class AugmentedRoadView(CameraView):
|
||||
self.alert_renderer = AlertRenderer()
|
||||
self.driver_state_renderer = DriverStateRenderer()
|
||||
|
||||
# Callbacks
|
||||
self.on_click: Callable | None = None
|
||||
|
||||
def render(self, rect):
|
||||
# Only render when system is started to avoid invalid data access
|
||||
if not ui_state.started:
|
||||
@@ -94,6 +97,12 @@ class AugmentedRoadView(CameraView):
|
||||
# End clipping region
|
||||
rl.end_scissor_mode()
|
||||
|
||||
# Handle click events if no HUD interaction occurred
|
||||
if not self._hud_renderer.handle_mouse_event():
|
||||
if self.on_click and rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
||||
if rl.check_collision_point_rec(rl.get_mouse_position(), self._content_rect):
|
||||
self.on_click()
|
||||
|
||||
def _draw_border(self, rect: rl.Rectangle):
|
||||
border_color = BORDER_COLORS.get(ui_state.status, BORDER_COLORS[UIStatus.DISENGAGED])
|
||||
rl.draw_rectangle_lines_ex(rect, UI_BORDER_SIZE, border_color)
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import time
|
||||
import pyray as rl
|
||||
from cereal.messaging import SubMaster
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.common.params import Params
|
||||
|
||||
|
||||
class ExpButton:
|
||||
def __init__(self, button_size: int, icon_size: int):
|
||||
self._params = Params()
|
||||
self._experimental_mode: bool = False
|
||||
self._engageable: bool = False
|
||||
|
||||
# State hold mechanism
|
||||
self._hold_duration = 2.0 # seconds
|
||||
self._held_mode: bool | None = None
|
||||
self._hold_end_time: float | None = None
|
||||
|
||||
self._white_color: rl.Color = rl.Color(255, 255, 255, 255)
|
||||
self._black_bg: rl.Color = rl.Color(0, 0, 0, 166)
|
||||
self._txt_wheel: rl.Texture = gui_app.texture('icons/chffr_wheel.png', icon_size, icon_size)
|
||||
self._txt_exp: rl.Texture = gui_app.texture('icons/experimental.png', icon_size, icon_size)
|
||||
self._rect: rl.Rectangle = rl.Rectangle(0, 0, button_size, button_size)
|
||||
|
||||
def update_state(self, sm: SubMaster) -> None:
|
||||
selfdrive_state = sm["selfdriveState"]
|
||||
self._experimental_mode = selfdrive_state.experimentalMode
|
||||
self._engageable = selfdrive_state.engageable or selfdrive_state.enabled
|
||||
|
||||
def handle_mouse_event(self) -> bool:
|
||||
if rl.check_collision_point_rec(rl.get_mouse_position(), self._rect):
|
||||
if (rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) and
|
||||
self._is_toggle_allowed()):
|
||||
new_mode = not self._experimental_mode
|
||||
self._params.put_bool("ExperimentalMode", new_mode)
|
||||
|
||||
# Hold new state temporarily
|
||||
self._held_mode = new_mode
|
||||
self._hold_end_time = time.time() + self._hold_duration
|
||||
return True
|
||||
return False
|
||||
|
||||
def draw(self, x: int, y: int) -> None:
|
||||
self._rect.x, self._rect.y = x, y
|
||||
center_x = int(self._rect.x + self._rect.width // 2)
|
||||
center_y = int(self._rect.y + self._rect.height // 2)
|
||||
|
||||
mouse_over = rl.check_collision_point_rec(rl.get_mouse_position(), self._rect)
|
||||
mouse_down = rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT)
|
||||
self._white_color.a = 180 if (mouse_down and mouse_over) or not self._engageable else 255
|
||||
|
||||
texture = self._txt_exp if self._held_or_actual_mode() else self._txt_wheel
|
||||
rl.draw_circle(center_x, center_y, self._rect.width / 2, self._black_bg)
|
||||
rl.draw_texture(texture, center_x - texture.width // 2, center_y - texture.height // 2, self._white_color)
|
||||
|
||||
def _held_or_actual_mode(self):
|
||||
now = time.time()
|
||||
if self._hold_end_time and now < self._hold_end_time:
|
||||
return self._held_mode
|
||||
|
||||
if self._hold_end_time and now >= self._hold_end_time:
|
||||
self._hold_end_time = self._held_mode = None
|
||||
|
||||
return self._experimental_mode
|
||||
|
||||
def _is_toggle_allowed(self):
|
||||
if not self._params.get_bool("ExperimentalModeConfirmed"):
|
||||
return False
|
||||
|
||||
car_params = ui_state.sm["carParams"]
|
||||
if car_params.alphaLongitudinalAvailable:
|
||||
return self._params.get_bool("AlphaLongitudinalEnabled")
|
||||
else:
|
||||
return car_params.openpilotLongitudinalControl
|
||||
@@ -2,6 +2,7 @@ import pyray as rl
|
||||
from dataclasses import dataclass
|
||||
from cereal.messaging import SubMaster
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
||||
from openpilot.selfdrive.ui.onroad.exp_button import ExpButton
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, Widget
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.common.conversions import Conversions as CV
|
||||
@@ -62,11 +63,13 @@ class HudRenderer(Widget):
|
||||
self.set_speed: float = SET_SPEED_NA
|
||||
self.speed: float = 0.0
|
||||
self.v_ego_cluster_seen: bool = False
|
||||
self._wheel_texture: rl.Texture = gui_app.texture('icons/chffr_wheel.png', UI_CONFIG.wheel_icon_size, UI_CONFIG.wheel_icon_size)
|
||||
|
||||
self._font_semi_bold: rl.Font = gui_app.font(FontWeight.SEMI_BOLD)
|
||||
self._font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
|
||||
self._font_medium: rl.Font = gui_app.font(FontWeight.MEDIUM)
|
||||
|
||||
self._exp_button = ExpButton(UI_CONFIG.button_size, UI_CONFIG.wheel_icon_size)
|
||||
|
||||
def _update_state(self, sm: SubMaster) -> None:
|
||||
"""Update HUD state based on car state and controls state."""
|
||||
if sm.recv_frame["carState"] < ui_state.started_frame:
|
||||
@@ -94,9 +97,13 @@ class HudRenderer(Widget):
|
||||
speed_conversion = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
|
||||
self.speed = max(0.0, v_ego * speed_conversion)
|
||||
|
||||
self._exp_button.update_state(sm)
|
||||
|
||||
def render(self, rect: rl.Rectangle) -> None:
|
||||
"""Render HUD elements to the screen."""
|
||||
self._update_state(ui_state.sm)
|
||||
|
||||
# Draw the header background
|
||||
rl.draw_rectangle_gradient_v(
|
||||
int(rect.x),
|
||||
int(rect.y),
|
||||
@@ -110,7 +117,13 @@ class HudRenderer(Widget):
|
||||
self._draw_set_speed(rect)
|
||||
|
||||
self._draw_current_speed(rect)
|
||||
self._draw_wheel_icon(rect)
|
||||
|
||||
button_x = rect.x + rect.width - UI_CONFIG.border_size - UI_CONFIG.button_size
|
||||
button_y = rect.y + UI_CONFIG.border_size
|
||||
self._exp_button.draw(button_x, button_y)
|
||||
|
||||
def handle_mouse_event(self) -> bool:
|
||||
return bool(self._exp_button.handle_mouse_event())
|
||||
|
||||
def _draw_set_speed(self, rect: rl.Rectangle) -> None:
|
||||
"""Draw the MAX speed indicator box."""
|
||||
@@ -166,13 +179,3 @@ class HudRenderer(Widget):
|
||||
unit_text_size = measure_text_cached(self._font_medium, unit_text, FONT_SIZES.speed_unit)
|
||||
unit_pos = rl.Vector2(rect.x + rect.width / 2 - unit_text_size.x / 2, 290 - unit_text_size.y / 2)
|
||||
rl.draw_text_ex(self._font_medium, unit_text, unit_pos, FONT_SIZES.speed_unit, 0, COLORS.white_translucent)
|
||||
|
||||
def _draw_wheel_icon(self, rect: rl.Rectangle) -> None:
|
||||
"""Draw the steering wheel icon with status-based opacity."""
|
||||
center_x = int(rect.x + rect.width - UI_CONFIG.border_size - UI_CONFIG.button_size / 2)
|
||||
center_y = int(rect.y + UI_CONFIG.border_size + UI_CONFIG.button_size / 2)
|
||||
rl.draw_circle(center_x, center_y, UI_CONFIG.button_size / 2, COLORS.black_translucent)
|
||||
|
||||
opacity = 0.7 if ui_state.status == UIStatus.DISENGAGED else 1.0
|
||||
img_pos = rl.Vector2(center_x - self._wheel_texture.width / 2, center_y - self._wheel_texture.height / 2)
|
||||
rl.draw_texture_v(self._wheel_texture, img_pos, rl.Color(255, 255, 255, int(255 * opacity)))
|
||||
|
||||
Reference in New Issue
Block a user