mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-02-18 21:14:01 +08:00
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
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user