raylib: add experimental mode + alpha long confirmation dialog + related fixes (#36295)

* here's everything

* just the dev part

* same for exp mode!

* use rich

* fix br not working in p

* html height needs to be different than content/text rect

* fix confirmation

* fix

* fix 2.5s lag

* clean up

* use correct param

* add offroad and engaged callback too

* nl

* lint
pull/36301/head
Shane Smiskol 5 days ago committed by GitHub
parent 0f40afa357
commit ddbbcc6f5d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 37
      selfdrive/ui/layouts/settings/developer.py
  2. 42
      selfdrive/ui/layouts/settings/toggles.py
  3. 25
      selfdrive/ui/ui_state.py
  4. 8
      system/ui/widgets/confirm_dialog.py
  5. 2
      system/ui/widgets/html_render.py

@ -4,6 +4,9 @@ from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.list_view import toggle_item
from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.widgets import DialogResult
# Description constants
DESCRIPTIONS = {
@ -80,6 +83,9 @@ class DeveloperLayout(Widget):
self._scroller = Scroller(items, line_separator=True, spacing=0)
# Toggles should be not available to change in onroad state
ui_state.add_offroad_transition_callback(self._update_toggles)
def _render(self, rect):
self._scroller.render(rect)
@ -87,7 +93,10 @@ class DeveloperLayout(Widget):
self._update_toggles()
def _update_toggles(self):
ui_state.update_params()
# Hide non-release toggles on release builds
# TODO: we can do an onroad cycle, but alpha long toggle requires a deinit function to re-enable radar and not fault
for item in (self._adb_toggle, self._joystick_toggle, self._long_maneuver_toggle, self._alpha_long_toggle):
item.set_visible(not self._is_release)
@ -100,7 +109,11 @@ class DeveloperLayout(Widget):
else:
self._alpha_long_toggle.set_visible(True)
self._long_maneuver_toggle.action_item.set_enabled(ui_state.has_longitudinal_control and ui_state.is_offroad)
long_man_enabled = ui_state.has_longitudinal_control and ui_state.is_offroad()
self._long_maneuver_toggle.action_item.set_enabled(long_man_enabled)
if not long_man_enabled:
self._long_maneuver_toggle.action_item.set_state(False)
self._params.put_bool("LongitudinalManeuverMode", False)
else:
self._long_maneuver_toggle.action_item.set_enabled(False)
self._alpha_long_toggle.set_visible(False)
@ -116,12 +129,6 @@ class DeveloperLayout(Widget):
):
item.action_item.set_state(self._params.get_bool(key))
def _update_state(self):
# Disable toggles that require onroad restart
# TODO: we can do an onroad cycle, but alpha long toggle requires a deinit function to re-enable radar and not fault
for item in (self._adb_toggle, self._joystick_toggle, self._long_maneuver_toggle, self._alpha_long_toggle):
item.action_item.set_enabled(ui_state.is_offroad)
def _on_enable_adb(self, state: bool):
self._params.put_bool("AdbEnabled", state)
@ -139,5 +146,21 @@ class DeveloperLayout(Widget):
self._joystick_toggle.action_item.set_state(False)
def _on_alpha_long_enabled(self, state: bool):
if state:
def confirm_callback(result: int):
if result == DialogResult.CONFIRM:
self._params.put_bool("AlphaLongitudinalEnabled", True)
self._update_toggles()
else:
self._alpha_long_toggle.action_item.set_state(False)
# show confirmation dialog
content = (f"<h2>{self._alpha_long_toggle.title}</h2><br>" +
f"<p>{self._alpha_long_toggle.description}</p>")
dlg = ConfirmDialog(content, "Enable", rich=True)
gui_app.set_modal_overlay(dlg, callback=confirm_callback)
return
self._params.put_bool("AlphaLongitudinalEnabled", state)
self._update_toggles()

@ -3,6 +3,9 @@ from openpilot.common.params import Params, UnknownKeyName
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.list_view import multiple_button_item, toggle_item
from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.widgets import DialogResult
from openpilot.selfdrive.ui.ui_state import ui_state
PERSONALITY_TO_INT = log.LongitudinalPersonality.schema.enumerants
@ -131,6 +134,8 @@ class TogglesLayout(Widget):
self._update_experimental_mode_icon()
self._scroller = Scroller(list(self._toggles.values()), line_separator=True, spacing=0)
ui_state.add_engaged_transition_callback(self._update_toggles)
def _update_state(self):
if ui_state.sm.updated["selfdriveState"]:
personality = PERSONALITY_TO_INT[ui_state.sm["selfdriveState"].personality]
@ -138,15 +143,12 @@ class TogglesLayout(Widget):
self._long_personality_setting.action_item.set_selected_button(personality)
ui_state.personality = personality
# these toggles need restart, block while engaged
for toggle_def in self._toggle_defs:
if self._toggle_defs[toggle_def][3] and toggle_def not in self._locked_toggles:
self._toggles[toggle_def].action_item.set_enabled(not ui_state.engaged)
def show_event(self):
self._update_toggles()
def _update_toggles(self):
ui_state.update_params()
e2e_description = (
"openpilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. " +
"Experimental features are listed below:<br>" +
@ -174,7 +176,7 @@ class TogglesLayout(Widget):
unavailable = "Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control."
long_desc = unavailable + " openpilot longitudinal control may come in a future update."
if ui_state.CP.getAlphaLongitudinalAvailable():
if ui_state.CP.alphaLongitudinalAvailable:
if self._is_release:
long_desc = unavailable + " " + ("An alpha version of openpilot longitudinal control can be tested, along with " +
"Experimental mode, on non-release branches.")
@ -192,6 +194,11 @@ class TogglesLayout(Widget):
for param in self._toggle_defs:
self._toggles[param].action_item.set_state(self._params.get_bool(param))
# these toggles need restart, block while engaged
for toggle_def in self._toggle_defs:
if self._toggle_defs[toggle_def][3] and toggle_def not in self._locked_toggles:
self._toggles[toggle_def].action_item.set_enabled(not ui_state.engaged)
def _render(self, rect):
self._scroller.render(rect)
@ -199,9 +206,30 @@ class TogglesLayout(Widget):
icon = "experimental.png" if self._toggles["ExperimentalMode"].action_item.get_state() else "experimental_white.png"
self._toggles["ExperimentalMode"].set_icon(icon)
def _handle_experimental_mode_toggle(self, state: bool):
confirmed = self._params.get_bool("ExperimentalModeConfirmed")
if state and not confirmed:
def confirm_callback(result: int):
if result == DialogResult.CONFIRM:
self._params.put_bool("ExperimentalMode", True)
self._params.put_bool("ExperimentalModeConfirmed", True)
else:
self._toggles["ExperimentalMode"].action_item.set_state(False)
self._update_experimental_mode_icon()
# show confirmation dialog
content = (f"<h2>{self._toggles['ExperimentalMode'].title}</h2><br>" +
f"<p>{self._toggles['ExperimentalMode'].description}</p>")
dlg = ConfirmDialog(content, "Enable", rich=True)
gui_app.set_modal_overlay(dlg, callback=confirm_callback)
else:
self._update_experimental_mode_icon()
self._params.put_bool("ExperimentalMode", state)
def _toggle_callback(self, state: bool, param: str):
if param == "ExperimentalMode":
self._update_experimental_mode_icon()
self._handle_experimental_mode_toggle(state)
return
self._params.put_bool(param, state)
if self._toggle_defs[param][3]:

@ -74,7 +74,17 @@ class UIState:
self.light_sensor: float = -1.0
self._param_update_time: float = 0.0
self._update_params()
# Callbacks
self._offroad_transition_callbacks: list[Callable[[], None]] = []
self._engaged_transition_callbacks: list[Callable[[], None]] = []
self.update_params()
def add_offroad_transition_callback(self, callback: Callable[[], None]):
self._offroad_transition_callbacks.append(callback)
def add_engaged_transition_callback(self, callback: Callable[[], None]):
self._engaged_transition_callbacks.append(callback)
@property
def engaged(self) -> bool:
@ -91,8 +101,7 @@ class UIState:
self._update_state()
self._update_status()
if time.monotonic() - self._param_update_time > 5.0:
self._update_params()
self._param_update_time = time.monotonic()
self.update_params()
device.update()
def _update_state(self) -> None:
@ -131,6 +140,8 @@ class UIState:
# Check for engagement state changes
if self.engaged != self._engaged_prev:
for callback in self._engaged_transition_callbacks:
callback()
self._engaged_prev = self.engaged
# Handle onroad/offroad transition
@ -140,19 +151,23 @@ class UIState:
self.started_frame = self.sm.frame
self.started_time = time.monotonic()
for callback in self._offroad_transition_callbacks:
callback()
self._started_prev = self.started
def _update_params(self) -> None:
def update_params(self) -> None:
self.is_metric = self.params.get_bool("IsMetric")
# Update longitudinal control state
CP_bytes = self.params.get("CarParams")
CP_bytes = self.params.get("CarParamsPersistent")
if CP_bytes is not None:
self.CP = messaging.log_from_bytes(CP_bytes, car.CarParams)
if self.CP.alphaLongitudinalAvailable:
self.has_longitudinal_control = self.params.get_bool("AlphaLongitudinalEnabled")
else:
self.has_longitudinal_control = self.CP.openpilotLongitudinalControl
self._param_update_time = time.monotonic()
class Device:

@ -3,7 +3,7 @@ from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.widgets import DialogResult
from openpilot.system.ui.widgets.button import ButtonStyle, Button
from openpilot.system.ui.widgets.label import Label
from openpilot.system.ui.widgets.html_render import HtmlRenderer
from openpilot.system.ui.widgets.html_render import HtmlRenderer, ElementType
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.scroller import Scroller
@ -19,7 +19,7 @@ class ConfirmDialog(Widget):
def __init__(self, text: str, confirm_text: str, cancel_text: str = "Cancel", rich: bool = False):
super().__init__()
self._label = Label(text, 70, FontWeight.BOLD, text_color=rl.Color(201, 201, 201, 255))
self._html_renderer = HtmlRenderer(text=text)
self._html_renderer = HtmlRenderer(text=text, text_size={ElementType.P: 50})
self._cancel_button = Button(cancel_text, self._cancel_button_callback)
self._confirm_button = Button(confirm_text, self._confirm_button_callback, button_style=ButtonStyle.PRIMARY)
self._rich = rich
@ -64,7 +64,9 @@ class ConfirmDialog(Widget):
if not self._rich:
self._label.render(text_rect)
else:
self._html_renderer.set_rect(text_rect)
html_rect = rl.Rectangle(text_rect.x, text_rect.y, text_rect.width,
self._html_renderer.get_total_height(int(text_rect.width)))
self._html_renderer.set_rect(html_rect)
self._scroller.render(text_rect)
if rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER):

@ -121,6 +121,8 @@ class HtmlRenderer(Widget):
is_start_tag, is_end_tag, tag = is_tag(token)
if tag is not None:
if tag == ElementType.BR:
# Close current tag and add a line break
close_tag()
self._add_element(ElementType.BR, "")
elif is_start_tag or is_end_tag:

Loading…
Cancel
Save