raylib: excessive actuation offroad alert (#36242)

* excessive actuation check

* from gpt

* back

* use buttons

* use widgets for ultimate clean up - no ai slop

* feature parity

* revert

* clean up
pull/36243/head
Shane Smiskol 2 days ago committed by GitHub
parent 75e52427d1
commit 21273c921e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 157
      selfdrive/ui/widgets/offroad_alerts.py

@ -1,10 +1,11 @@
import pyray as rl import pyray as rl
from enum import IntEnum
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.system.hardware import HARDWARE from openpilot.system.hardware import HARDWARE
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.lib.wrap_text import wrap_text
@ -17,15 +18,16 @@ class AlertColors:
LOW_SEVERITY = rl.Color(41, 41, 41, 255) LOW_SEVERITY = rl.Color(41, 41, 41, 255)
BACKGROUND = rl.Color(57, 57, 57, 255) BACKGROUND = rl.Color(57, 57, 57, 255)
BUTTON = rl.WHITE BUTTON = rl.WHITE
BUTTON_PRESSED = rl.Color(200, 200, 200, 255)
BUTTON_TEXT = rl.BLACK BUTTON_TEXT = rl.BLACK
SNOOZE_BG = rl.Color(79, 79, 79, 255) SNOOZE_BG = rl.Color(79, 79, 79, 255)
SNOOZE_BG_PRESSED = rl.Color(100, 100, 100, 255)
TEXT = rl.WHITE TEXT = rl.WHITE
class AlertConstants: class AlertConstants:
BUTTON_SIZE = (400, 125) MIN_BUTTON_WIDTH = 400
SNOOZE_BUTTON_SIZE = (550, 125) BUTTON_HEIGHT = 125
REBOOT_BUTTON_SIZE = (600, 125)
MARGIN = 50 MARGIN = 50
SPACING = 30 SPACING = 30
FONT_SIZE = 48 FONT_SIZE = 48
@ -43,6 +45,41 @@ class AlertData:
visible: bool = False visible: bool = False
class ButtonStyle(IntEnum):
LIGHT = 0
DARK = 1
class ActionButton(Widget):
def __init__(self, text: str, style: ButtonStyle = ButtonStyle.LIGHT,
min_width: int = AlertConstants.MIN_BUTTON_WIDTH):
super().__init__()
self._style = style
self._min_width = min_width
self._font = gui_app.font(FontWeight.MEDIUM)
self.set_text(text)
def set_text(self, text: str):
self._text = text
self._text_width = measure_text_cached(gui_app.font(FontWeight.MEDIUM), self._text, AlertConstants.FONT_SIZE).x
self._rect.width = max(self._text_width + 60 * 2, self._min_width)
self._rect.height = AlertConstants.BUTTON_HEIGHT
def _render(self, _):
roundness = AlertConstants.BORDER_RADIUS / self._rect.height
bg_color = AlertColors.BUTTON if self._style == ButtonStyle.LIGHT else AlertColors.SNOOZE_BG
if self.is_pressed:
bg_color = AlertColors.BUTTON_PRESSED if self._style == ButtonStyle.LIGHT else AlertColors.SNOOZE_BG_PRESSED
rl.draw_rectangle_rounded(self._rect, roundness, 10, bg_color)
# center text
color = rl.WHITE if self._style == ButtonStyle.DARK else rl.BLACK
text_x = int(self._rect.x + (self._rect.width - self._text_width) // 2)
text_y = int(self._rect.y + (self._rect.height - AlertConstants.FONT_SIZE) // 2)
rl.draw_text_ex(self._font, self._text, rl.Vector2(text_x, text_y), AlertConstants.FONT_SIZE, 0, color)
class AbstractAlert(Widget, ABC): class AbstractAlert(Widget, ABC):
def __init__(self, has_reboot_btn: bool = False): def __init__(self, has_reboot_btn: bool = False):
super().__init__() super().__init__()
@ -50,17 +87,35 @@ class AbstractAlert(Widget, ABC):
self.has_reboot_btn = has_reboot_btn self.has_reboot_btn = has_reboot_btn
self.dismiss_callback: Callable | None = None self.dismiss_callback: Callable | None = None
self.dismiss_btn_rect = rl.Rectangle(0, 0, *AlertConstants.BUTTON_SIZE) def snooze_callback():
self.snooze_btn_rect = rl.Rectangle(0, 0, *AlertConstants.SNOOZE_BUTTON_SIZE) self.params.put_bool("SnoozeUpdate", True)
self.reboot_btn_rect = rl.Rectangle(0, 0, *AlertConstants.REBOOT_BUTTON_SIZE) if self.dismiss_callback:
self.dismiss_callback()
def excessive_actuation_callback():
self.params.remove("Offroad_ExcessiveActuation")
if self.dismiss_callback:
self.dismiss_callback()
self.dismiss_btn = ActionButton("Close")
self.snooze_btn = ActionButton("Snooze Update", style=ButtonStyle.DARK)
self.snooze_btn.set_click_callback(snooze_callback)
self.excessive_actuation_btn = ActionButton("Acknowledge Excessive Actuation", style=ButtonStyle.DARK, min_width=800)
self.excessive_actuation_btn.set_click_callback(excessive_actuation_callback)
self.reboot_btn = ActionButton("Reboot and Update", min_width=600)
self.reboot_btn.set_click_callback(lambda: HARDWARE.reboot())
self.snooze_visible = False # TODO: just use a Scroller?
self.content_rect = rl.Rectangle(0, 0, 0, 0) self.content_rect = rl.Rectangle(0, 0, 0, 0)
self.scroll_panel_rect = rl.Rectangle(0, 0, 0, 0) self.scroll_panel_rect = rl.Rectangle(0, 0, 0, 0)
self.scroll_panel = GuiScrollPanel() self.scroll_panel = GuiScrollPanel()
def set_dismiss_callback(self, callback: Callable): def set_dismiss_callback(self, callback: Callable):
self.dismiss_callback = callback self.dismiss_callback = callback
self.dismiss_btn.set_click_callback(self.dismiss_callback)
@abstractmethod @abstractmethod
def refresh(self) -> bool: def refresh(self) -> bool:
@ -70,28 +125,10 @@ class AbstractAlert(Widget, ABC):
def get_content_height(self) -> float: def get_content_height(self) -> float:
pass pass
def _handle_mouse_release(self, mouse_pos: MousePos):
super()._handle_mouse_release(mouse_pos)
if not self.scroll_panel.is_touch_valid():
return
if rl.check_collision_point_rec(mouse_pos, self.dismiss_btn_rect):
if self.dismiss_callback:
self.dismiss_callback()
elif self.snooze_visible and rl.check_collision_point_rec(mouse_pos, self.snooze_btn_rect):
self.params.put_bool("SnoozeUpdate", True)
if self.dismiss_callback:
self.dismiss_callback()
elif self.has_reboot_btn and rl.check_collision_point_rec(mouse_pos, self.reboot_btn_rect):
HARDWARE.reboot()
def _render(self, rect: rl.Rectangle): def _render(self, rect: rl.Rectangle):
rl.draw_rectangle_rounded(rect, AlertConstants.BORDER_RADIUS / rect.height, 10, AlertColors.BACKGROUND) rl.draw_rectangle_rounded(rect, AlertConstants.BORDER_RADIUS / rect.height, 10, AlertColors.BACKGROUND)
footer_height = AlertConstants.BUTTON_SIZE[1] + AlertConstants.SPACING footer_height = AlertConstants.BUTTON_HEIGHT + AlertConstants.SPACING
content_height = rect.height - 2 * AlertConstants.MARGIN - footer_height content_height = rect.height - 2 * AlertConstants.MARGIN - footer_height
self.content_rect = rl.Rectangle( self.content_rect = rl.Rectangle(
@ -134,47 +171,26 @@ class AbstractAlert(Widget, ABC):
pass pass
def _render_footer(self, rect: rl.Rectangle): def _render_footer(self, rect: rl.Rectangle):
footer_y = rect.y + rect.height - AlertConstants.MARGIN - AlertConstants.BUTTON_SIZE[1] footer_y = rect.y + rect.height - AlertConstants.MARGIN - AlertConstants.BUTTON_HEIGHT
font = gui_app.font(FontWeight.MEDIUM)
self.dismiss_btn_rect.x = rect.x + AlertConstants.MARGIN
self.dismiss_btn_rect.y = footer_y
roundness = AlertConstants.BORDER_RADIUS / self.dismiss_btn_rect.height
rl.draw_rectangle_rounded(self.dismiss_btn_rect, roundness, 10, AlertColors.BUTTON)
text = "Close"
text_width = measure_text_cached(font, text, AlertConstants.FONT_SIZE).x
text_x = self.dismiss_btn_rect.x + (AlertConstants.BUTTON_SIZE[0] - text_width) // 2
text_y = self.dismiss_btn_rect.y + (AlertConstants.BUTTON_SIZE[1] - AlertConstants.FONT_SIZE) // 2
rl.draw_text_ex(
font, text, rl.Vector2(int(text_x), int(text_y)), AlertConstants.FONT_SIZE, 0, AlertColors.BUTTON_TEXT
)
if self.snooze_visible: dismiss_x = rect.x + AlertConstants.MARGIN
self.snooze_btn_rect.x = rect.x + rect.width - AlertConstants.MARGIN - AlertConstants.SNOOZE_BUTTON_SIZE[0] self.dismiss_btn.set_position(dismiss_x, footer_y)
self.snooze_btn_rect.y = footer_y self.dismiss_btn.render()
roundness = AlertConstants.BORDER_RADIUS / self.snooze_btn_rect.height
rl.draw_rectangle_rounded(self.snooze_btn_rect, roundness, 10, AlertColors.SNOOZE_BG) if self.has_reboot_btn:
reboot_x = rect.x + rect.width - AlertConstants.MARGIN - self.reboot_btn.rect.width
text = "Snooze Update" self.reboot_btn.set_position(reboot_x, footer_y)
text_width = measure_text_cached(font, text, AlertConstants.FONT_SIZE).x self.reboot_btn.render()
text_x = self.snooze_btn_rect.x + (AlertConstants.SNOOZE_BUTTON_SIZE[0] - text_width) // 2
text_y = self.snooze_btn_rect.y + (AlertConstants.SNOOZE_BUTTON_SIZE[1] - AlertConstants.FONT_SIZE) // 2 elif self.excessive_actuation_btn.is_visible:
rl.draw_text_ex(font, text, rl.Vector2(int(text_x), int(text_y)), AlertConstants.FONT_SIZE, 0, AlertColors.TEXT) actuation_x = rect.x + rect.width - AlertConstants.MARGIN - self.excessive_actuation_btn.rect.width
self.excessive_actuation_btn.set_position(actuation_x, footer_y)
elif self.has_reboot_btn: self.excessive_actuation_btn.render()
self.reboot_btn_rect.x = rect.x + rect.width - AlertConstants.MARGIN - AlertConstants.REBOOT_BUTTON_SIZE[0]
self.reboot_btn_rect.y = footer_y elif self.snooze_btn.is_visible:
roundness = AlertConstants.BORDER_RADIUS / self.reboot_btn_rect.height snooze_x = rect.x + rect.width - AlertConstants.MARGIN - self.snooze_btn.rect.width
rl.draw_rectangle_rounded(self.reboot_btn_rect, roundness, 10, AlertColors.BUTTON) self.snooze_btn.set_position(snooze_x, footer_y)
self.snooze_btn.render()
text = "Reboot and Update"
text_width = measure_text_cached(font, text, AlertConstants.FONT_SIZE).x
text_x = self.reboot_btn_rect.x + (AlertConstants.REBOOT_BUTTON_SIZE[0] - text_width) // 2
text_y = self.reboot_btn_rect.y + (AlertConstants.REBOOT_BUTTON_SIZE[1] - AlertConstants.FONT_SIZE) // 2
rl.draw_text_ex(
font, text, rl.Vector2(int(text_x), int(text_y)), AlertConstants.FONT_SIZE, 0, AlertColors.BUTTON_TEXT
)
class OffroadAlert(AbstractAlert): class OffroadAlert(AbstractAlert):
@ -188,6 +204,7 @@ class OffroadAlert(AbstractAlert):
active_count = 0 active_count = 0
connectivity_needed = False connectivity_needed = False
excessive_actuation = False
for alert_data in self.sorted_alerts: for alert_data in self.sorted_alerts:
text = "" text = ""
@ -205,7 +222,11 @@ class OffroadAlert(AbstractAlert):
if alert_data.key == "Offroad_ConnectivityNeeded" and alert_data.visible: if alert_data.key == "Offroad_ConnectivityNeeded" and alert_data.visible:
connectivity_needed = True connectivity_needed = True
self.snooze_visible = connectivity_needed if alert_data.key == "Offroad_ExcessiveActuation" and alert_data.visible:
excessive_actuation = True
self.excessive_actuation_btn.set_visible(excessive_actuation)
self.snooze_btn.set_visible(connectivity_needed and not excessive_actuation)
return active_count return active_count
def get_content_height(self) -> float: def get_content_height(self) -> float:

Loading…
Cancel
Save