pull/35398/head
Yassine Yousfi 3 months ago
commit 5df6150894
  1. 2
      opendbc_repo
  2. 2
      panda
  3. 2
      selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py
  4. 2
      selfdrive/test/process_replay/ref_commit
  5. 4
      selfdrive/test/process_replay/test_processes.py
  6. 3
      selfdrive/test/setup_device_ci.sh
  7. BIN
      selfdrive/ui/_spinner
  8. BIN
      selfdrive/ui/_text
  9. 27
      system/ui/lib/application.py
  10. 21
      system/ui/lib/button.py
  11. 157
      system/ui/lib/inputbox.py
  12. 24
      system/ui/lib/label.py
  13. 21
      system/ui/lib/wifi_manager.py
  14. 52
      system/ui/updater.py
  15. 87
      system/ui/widgets/cameraview.py
  16. 16
      system/ui/widgets/confirm_dialog.py
  17. 62
      system/ui/widgets/keyboard.py
  18. 41
      system/ui/widgets/network.py
  19. 81
      system/ui/widgets/option_dialog.py
  20. 31
      tools/clip/run.py
  21. 1
      tools/install_ubuntu_dependencies.sh
  22. 2
      tools/op.sh
  23. 1316
      uv.lock

@ -1 +1 @@
Subproject commit c856a2c0bd2b3c75f86a73b051c0c4cc7159559e Subproject commit 95ee4edd17ecc6700eac12f2074de6b5478b9477

@ -1 +1 @@
Subproject commit b4773f96b38a56089b28bf70b8073a9ddce6d847 Subproject commit 7eb5dba3dc9960373128244c10fac99bba91f630

@ -54,7 +54,7 @@ T_IDXS = np.array(T_IDXS_LST)
FCW_IDXS = T_IDXS < 5.0 FCW_IDXS = T_IDXS < 5.0
T_DIFFS = np.diff(T_IDXS, prepend=[0.]) T_DIFFS = np.diff(T_IDXS, prepend=[0.])
COMFORT_BRAKE = 2.5 COMFORT_BRAKE = 2.5
STOP_DISTANCE = 6.0 STOP_DISTANCE = 4.0
CRUISE_MIN_ACCEL = -1.2 CRUISE_MIN_ACCEL = -1.2
CRUISE_MAX_ACCEL = 1.6 CRUISE_MAX_ACCEL = 1.6

@ -1 +1 @@
7bf4ae5b92a3ad1f073f675e24e28babad0f2aa0 b31b7c5c29e6d30ccee2fa5105af778810fcd02e

@ -35,6 +35,7 @@ source_segments = [
("MAZDA", "bd6a637565e91581|2021-10-30--15-14-53--4"), # MAZDA.MAZDA_CX9_2021 ("MAZDA", "bd6a637565e91581|2021-10-30--15-14-53--4"), # MAZDA.MAZDA_CX9_2021
("FORD", "54827bf84c38b14f|2023-01-26--21-59-07--4"), # FORD.FORD_BRONCO_SPORT_MK1 ("FORD", "54827bf84c38b14f|2023-01-26--21-59-07--4"), # FORD.FORD_BRONCO_SPORT_MK1
("RIVIAN", "bc095dc92e101734|000000db--ee9fe46e57--1"), # RIVIAN.RIVIAN_R1_GEN1 ("RIVIAN", "bc095dc92e101734|000000db--ee9fe46e57--1"), # RIVIAN.RIVIAN_R1_GEN1
("TESLA", "2c912ca5de3b1ee9|0000025d--6eb6bcbca4--4"), # TESLA.TESLA_MODEL_Y
# Enable when port is tested and dashcamOnly is no longer set # Enable when port is tested and dashcamOnly is no longer set
#("VOLKSWAGEN2", "3cfdec54aa035f3f|2022-07-19--23-45-10--2"), # VOLKSWAGEN.VOLKSWAGEN_PASSAT_NMS #("VOLKSWAGEN2", "3cfdec54aa035f3f|2022-07-19--23-45-10--2"), # VOLKSWAGEN.VOLKSWAGEN_PASSAT_NMS
@ -58,6 +59,7 @@ segments = [
("MAZDA", "regenACF84CCF482|2024-08-30--03-21-55--0"), ("MAZDA", "regenACF84CCF482|2024-08-30--03-21-55--0"),
("FORD", "regen755D8CB1E1F|2025-04-08--23-13-43--0"), ("FORD", "regen755D8CB1E1F|2025-04-08--23-13-43--0"),
("RIVIAN", "regen5FCAC896BBE|2025-04-08--23-13-35--0"), ("RIVIAN", "regen5FCAC896BBE|2025-04-08--23-13-35--0"),
("TESLA", "2c912ca5de3b1ee9|0000025d--6eb6bcbca4--4"),
] ]
# dashcamOnly makes don't need to be tested until a full port is done # dashcamOnly makes don't need to be tested until a full port is done
@ -195,7 +197,7 @@ if __name__ == "__main__":
continue continue
# to speed things up, we only test all segments on card # to speed things up, we only test all segments on card
if cfg.proc_name != 'card' and car_brand not in ('HYUNDAI', 'TOYOTA', 'HONDA', 'SUBARU', 'FORD', 'RIVIAN'): if cfg.proc_name != 'card' and car_brand not in ('HYUNDAI', 'TOYOTA', 'HONDA', 'SUBARU', 'FORD', 'RIVIAN', 'TESLA'):
continue continue
cur_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{cur_commit}.zst") cur_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{cur_commit}.zst")

@ -18,6 +18,9 @@ if [ -z "$TEST_DIR" ]; then
exit 1 exit 1
fi fi
# prevent storage from filling up
rm -rf /data/media/0/realdata/*
rm -rf /data/safe_staging/ || true rm -rf /data/safe_staging/ || true
if [ -d /data/safe_staging/ ]; then if [ -d /data/safe_staging/ ]; then
sudo umount /data/safe_staging/merged/ || true sudo umount /data/safe_staging/merged/ || true

Binary file not shown.

Binary file not shown.

@ -40,6 +40,7 @@ class GuiApplication:
self._target_fps: int = DEFAULT_FPS self._target_fps: int = DEFAULT_FPS
self._last_fps_log_time: float = time.monotonic() self._last_fps_log_time: float = time.monotonic()
self._window_close_requested = False self._window_close_requested = False
self._trace_log_callback = None
def request_close(self): def request_close(self):
self._window_close_requested = True self._window_close_requested = True
@ -50,6 +51,9 @@ class GuiApplication:
HARDWARE.set_display_power(True) HARDWARE.set_display_power(True)
HARDWARE.set_screen_brightness(65) HARDWARE.set_screen_brightness(65)
self._set_log_callback()
rl.set_trace_log_level(rl.TraceLogLevel.LOG_ALL)
rl.set_config_flags(rl.ConfigFlags.FLAG_MSAA_4X_HINT | rl.ConfigFlags.FLAG_VSYNC_HINT) rl.set_config_flags(rl.ConfigFlags.FLAG_MSAA_4X_HINT | rl.ConfigFlags.FLAG_VSYNC_HINT)
rl.init_window(self._width, self._height, title) rl.init_window(self._width, self._height, title)
rl.set_target_fps(fps) rl.set_target_fps(fps)
@ -137,6 +141,29 @@ class GuiApplication:
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_COLOR_NORMAL, rl.color_to_int(DEFAULT_TEXT_COLOR)) rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_COLOR_NORMAL, rl.color_to_int(DEFAULT_TEXT_COLOR))
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.BASE_COLOR_NORMAL, rl.color_to_int(rl.Color(50, 50, 50, 255))) rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.BASE_COLOR_NORMAL, rl.color_to_int(rl.Color(50, 50, 50, 255)))
def _set_log_callback(self):
@rl.ffi.callback("void(int, char *, void *)")
def trace_log_callback(log_level, text, args):
try:
text_str = rl.ffi.string(text).decode('utf-8')
except (TypeError, UnicodeDecodeError):
text_str = str(text)
if log_level == rl.TraceLogLevel.LOG_ERROR:
cloudlog.error(f"raylib: {text_str}")
elif log_level == rl.TraceLogLevel.LOG_WARNING:
cloudlog.warning(f"raylib: {text_str}")
elif log_level == rl.TraceLogLevel.LOG_INFO:
cloudlog.info(f"raylib: {text_str}")
elif log_level == rl.TraceLogLevel.LOG_DEBUG:
cloudlog.debug(f"raylib: {text_str}")
else:
cloudlog.error(f"raylib: Unknown level {log_level}: {text_str}")
# Store callback reference
self._trace_log_callback = trace_log_callback
rl.set_trace_log_callback(self._trace_log_callback)
def _monitor_fps(self): def _monitor_fps(self):
fps = rl.get_fps() fps = rl.get_fps()

@ -10,6 +10,12 @@ class ButtonStyle(IntEnum):
TRANSPARENT = 3 # For buttons with transparent background and border TRANSPARENT = 3 # For buttons with transparent background and border
class TextAlignment(IntEnum):
LEFT = 0
CENTER = 1
RIGHT = 2
DEFAULT_BUTTON_FONT_SIZE = 60 DEFAULT_BUTTON_FONT_SIZE = 60
BUTTON_ENABLED_TEXT_COLOR = rl.Color(228, 228, 228, 255) BUTTON_ENABLED_TEXT_COLOR = rl.Color(228, 228, 228, 255)
BUTTON_DISABLED_TEXT_COLOR = rl.Color(228, 228, 228, 51) BUTTON_DISABLED_TEXT_COLOR = rl.Color(228, 228, 228, 51)
@ -38,6 +44,8 @@ def gui_button(
button_style: ButtonStyle = ButtonStyle.NORMAL, button_style: ButtonStyle = ButtonStyle.NORMAL,
is_enabled: bool = True, is_enabled: bool = True,
border_radius: int = 10, # Corner rounding in pixels border_radius: int = 10, # Corner rounding in pixels
text_alignment: TextAlignment = TextAlignment.CENTER,
text_padding: int = 20, # Padding for left/right alignment
) -> int: ) -> int:
result = 0 result = 0
@ -58,11 +66,16 @@ def gui_button(
rl.draw_rectangle_rounded_lines_ex(rect, roundness, 20, 2, rl.WHITE) rl.draw_rectangle_rounded_lines_ex(rect, roundness, 20, 2, rl.WHITE)
font = gui_app.font(font_weight) font = gui_app.font(font_weight)
# Center text in the button
text_size = rl.measure_text_ex(font, text, font_size, 0) text_size = rl.measure_text_ex(font, text, font_size, 0)
text_pos = rl.Vector2( text_pos = rl.Vector2(0, rect.y + (rect.height - text_size.y) // 2) # Vertical centering
rect.x + (rect.width - text_size.x) // 2, rect.y + (rect.height - text_size.y) // 2
) # Horizontal alignment
if text_alignment == TextAlignment.LEFT:
text_pos.x = rect.x + text_padding
elif text_alignment == TextAlignment.CENTER:
text_pos.x = rect.x + (rect.width - text_size.x) // 2
elif text_alignment == TextAlignment.RIGHT:
text_pos.x = rect.x + rect.width - text_size.x - text_padding
# Draw the button text # Draw the button text
text_color = BUTTON_ENABLED_TEXT_COLOR if is_enabled else BUTTON_DISABLED_TEXT_COLOR text_color = BUTTON_ENABLED_TEXT_COLOR if is_enabled else BUTTON_DISABLED_TEXT_COLOR

@ -0,0 +1,157 @@
import pyray as rl
from openpilot.system.ui.lib.application import gui_app
class InputBox:
def __init__(self, max_text_size=255, password_mode=False):
self._max_text_size = max_text_size
self._input_text = ""
self._cursor_position = 0
self._password_mode = password_mode
self._blink_counter = 0
self._show_cursor = False
self._last_key_pressed = 0
self._key_press_time = 0
self._repeat_delay = 30
self._repeat_rate = 5
@property
def text(self):
return self._input_text
@text.setter
def text(self, value):
self._input_text = value[: self._max_text_size]
self._cursor_position = len(self._input_text)
def set_password_mode(self, password_mode):
self._password_mode = password_mode
def clear(self):
self._input_text = ''
self._cursor_position = 0
def set_cursor_position(self, position):
"""Set the cursor position and reset the blink counter."""
if 0 <= position <= len(self._input_text):
self._cursor_position = position
self._blink_counter = 0
self._show_cursor = True
def add_char_at_cursor(self, char):
"""Add a character at the current cursor position."""
if len(self._input_text) < self._max_text_size:
self._input_text = self._input_text[: self._cursor_position] + char + self._input_text[self._cursor_position :]
self.set_cursor_position(self._cursor_position + 1)
return True
return False
def delete_char_before_cursor(self):
"""Delete the character before the cursor position (backspace)."""
if self._cursor_position > 0:
self._input_text = self._input_text[: self._cursor_position - 1] + self._input_text[self._cursor_position :]
self.set_cursor_position(self._cursor_position - 1)
return True
return False
def delete_char_at_cursor(self):
"""Delete the character at the cursor position (delete)."""
if self._cursor_position < len(self._input_text):
self._input_text = self._input_text[: self._cursor_position] + self._input_text[self._cursor_position + 1 :]
self.set_cursor_position(self._cursor_position)
return True
return False
def render(self, rect, color=rl.LIGHTGRAY, border_color=rl.DARKGRAY, text_color=rl.BLACK, font_size=80):
# Handle mouse input
self._handle_mouse_input(rect, font_size)
# Draw input box
rl.draw_rectangle_rec(rect, color)
rl.draw_rectangle_lines_ex(rect, 1, border_color)
# Process keyboard input
self._handle_keyboard_input()
# Update cursor blink
self._blink_counter += 1
if self._blink_counter >= 30:
self._show_cursor = not self._show_cursor
self._blink_counter = 0
# Display text
font = gui_app.font()
display_text = "" * len(self._input_text) if self._password_mode else self._input_text
padding = 10
rl.draw_text_ex(
font,
display_text,
rl.Vector2(int(rect.x + padding), int(rect.y + rect.height / 2 - font_size / 2)),
font_size,
0,
text_color,
)
# Draw cursor
if self._show_cursor:
cursor_x = rect.x + padding
if len(display_text) > 0 and self._cursor_position > 0:
cursor_x += rl.measure_text_ex(font, display_text[: self._cursor_position], font_size, 0).x
cursor_height = font_size + 4
cursor_y = rect.y + rect.height / 2 - cursor_height / 2
rl.draw_line(int(cursor_x), int(cursor_y), int(cursor_x), int(cursor_y + cursor_height), rl.BLACK)
def _handle_mouse_input(self, rect, font_size):
"""Handle mouse clicks to position cursor."""
mouse_pos = rl.get_mouse_position()
if rl.is_mouse_button_pressed(rl.MOUSE_LEFT_BUTTON) and rl.check_collision_point_rec(mouse_pos, rect):
# Calculate cursor position from click
if len(self._input_text) > 0:
text_width = rl.measure_text_ex(gui_app.font(), self._input_text, font_size, 0).x
text_pos_x = rect.x + 10
if mouse_pos.x - text_pos_x > text_width:
self.set_cursor_position(len(self._input_text))
else:
click_ratio = (mouse_pos.x - text_pos_x) / text_width
self.set_cursor_position(int(len(self._input_text) * click_ratio))
else:
self.set_cursor_position(0)
def _handle_keyboard_input(self):
"""Process keyboard input."""
key = rl.get_key_pressed()
# Handle key repeats
if key == self._last_key_pressed and key != 0:
self._key_press_time += 1
if self._key_press_time > self._repeat_delay and self._key_press_time % self._repeat_rate == 0:
# Process repeated key
pass
else:
return # Skip processing until repeat triggers
else:
self._last_key_pressed = key
self._key_press_time = 0
# Handle navigation keys
if key == rl.KEY_LEFT:
if self._cursor_position > 0:
self.set_cursor_position(self._cursor_position - 1)
elif key == rl.KEY_RIGHT:
if self._cursor_position < len(self._input_text):
self.set_cursor_position(self._cursor_position + 1)
elif key == rl.KEY_BACKSPACE:
self.delete_char_before_cursor()
elif key == rl.KEY_DELETE:
self.delete_char_at_cursor()
elif key == rl.KEY_HOME:
self.set_cursor_position(0)
elif key == rl.KEY_END:
self.set_cursor_position(len(self._input_text))
# Handle text input
char = rl.get_char_pressed()
if char != 0 and char >= 32: # Filter out control characters
self.add_char_at_cursor(chr(char))

@ -10,13 +10,27 @@ def gui_label(
color: rl.Color = DEFAULT_TEXT_COLOR, color: rl.Color = DEFAULT_TEXT_COLOR,
font_weight: FontWeight = FontWeight.NORMAL, font_weight: FontWeight = FontWeight.NORMAL,
alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT, alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
elide_right: bool = True
): ):
# Set font based on the provided weight
font = gui_app.font(font_weight) font = gui_app.font(font_weight)
# Measure text size
text_size = rl.measure_text_ex(font, text, font_size, 0) text_size = rl.measure_text_ex(font, text, font_size, 0)
display_text = text
# Elide text to fit within the rectangle
if elide_right and text_size.x > rect.width:
ellipsis = "..."
left, right = 0, len(text)
while left < right:
mid = (left + right) // 2
candidate = text[:mid] + ellipsis
candidate_size = rl.measure_text_ex(font, candidate, font_size, 0)
if candidate_size.x <= rect.width:
left = mid + 1
else:
right = mid
display_text = text[: left - 1] + ellipsis if left > 0 else ellipsis
text_size = rl.measure_text_ex(font, display_text, font_size, 0)
# Calculate horizontal position based on alignment # Calculate horizontal position based on alignment
text_x = rect.x + { text_x = rect.x + {
@ -33,7 +47,7 @@ def gui_label(
}.get(alignment_vertical, 0) }.get(alignment_vertical, 0)
# Draw the text in the specified rectangle # Draw the text in the specified rectangle
rl.draw_text_ex(font, text, rl.Vector2(text_x, text_y), font_size, 0, color) rl.draw_text_ex(font, display_text, rl.Vector2(text_x, text_y), font_size, 0, color)
def gui_text_box( def gui_text_box(

@ -1,5 +1,6 @@
import asyncio import asyncio
import concurrent.futures import concurrent.futures
import copy
import threading import threading
import time import time
import uuid import uuid
@ -56,6 +57,7 @@ class NetworkInfo:
security_type: SecurityType security_type: SecurityType
path: str path: str
bssid: str bssid: str
is_saved: bool = False
# saved_path: str # saved_path: str
@ -64,6 +66,7 @@ class WifiManagerCallbacks:
need_auth: Callable[[str], None] | None = None need_auth: Callable[[str], None] | None = None
activated: Callable[[], None] | None = None activated: Callable[[], None] | None = None
forgotten: Callable[[], None] | None = None forgotten: Callable[[], None] | None = None
networks_updated: Callable[[list[NetworkInfo]], None] | None = None
class WifiManager: class WifiManager:
@ -76,7 +79,11 @@ class WifiManager:
self.saved_connections: dict[str, str] = {} self.saved_connections: dict[str, str] = {}
self.active_ap_path: str = "" self.active_ap_path: str = ""
self.scan_task: asyncio.Task | None = None self.scan_task: asyncio.Task | None = None
self._tethering_ssid = "weedle-" + Params().get("DongleId", encoding="utf-8") # Set tethering ssid as "weedle" + first 4 characters of a dongle id
self._tethering_ssid = "weedle"
dongle_id = Params().get("DongleId", encoding="utf-8")
if dongle_id:
self._tethering_ssid += "-" + dongle_id[:4]
self.running: bool = True self.running: bool = True
self._current_connection_ssid: str | None = None self._current_connection_ssid: str | None = None
@ -452,6 +459,8 @@ class WifiManager:
del self.saved_connections[ssid] del self.saved_connections[ssid]
if self.callbacks.forgotten: if self.callbacks.forgotten:
self.callbacks.forgotten() self.callbacks.forgotten()
# Update network list to reflect the removed saved connection
asyncio.create_task(self._update_connection_status())
break break
async def _add_saved_connection(self, path: str) -> None: async def _add_saved_connection(self, path: str) -> None:
@ -460,6 +469,7 @@ class WifiManager:
settings = await self._get_connection_settings(path) settings = await self._get_connection_settings(path)
if ssid := self._extract_ssid(settings): if ssid := self._extract_ssid(settings):
self.saved_connections[ssid] = path self.saved_connections[ssid] = path
await self._update_connection_status()
except DBusError as e: except DBusError as e:
cloudlog.error(f"Failed to add connection {path}: {e}") cloudlog.error(f"Failed to add connection {path}: {e}")
@ -517,6 +527,7 @@ class WifiManager:
path=ap_path, path=ap_path,
bssid=bssid, bssid=bssid,
is_connected=self.active_ap_path == ap_path, is_connected=self.active_ap_path == ap_path,
is_saved=ssid in self.saved_connections
) )
except DBusError as e: except DBusError as e:
@ -533,6 +544,9 @@ class WifiManager:
), ),
) )
if self.callbacks.networks_updated:
self.callbacks.networks_updated(copy.deepcopy(self.networks))
async def _get_connection_settings(self, path): async def _get_connection_settings(self, path):
"""Fetch connection settings for a specific connection path.""" """Fetch connection settings for a specific connection path."""
try: try:
@ -628,11 +642,6 @@ class WifiManagerWrapper:
self._thread.join(timeout=2.0) self._thread.join(timeout=2.0)
self._running = False self._running = False
@property
def networks(self) -> list[NetworkInfo]:
"""Get the current list of networks."""
return self._run_coroutine_sync(lambda manager: manager.networks.copy(), default=[])
def is_saved(self, ssid: str) -> bool: def is_saved(self, ssid: str) -> bool:
"""Check if a network is saved.""" """Check if a network is saved."""
return self._run_coroutine_sync(lambda manager: manager.is_saved(ssid), default=False) return self._run_coroutine_sync(lambda manager: manager.is_saved(ssid), default=False)

@ -9,6 +9,9 @@ from openpilot.system.hardware import HARDWARE
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.button import gui_button, ButtonStyle from openpilot.system.ui.lib.button import gui_button, ButtonStyle
from openpilot.system.ui.lib.label import gui_text_box, gui_label from openpilot.system.ui.lib.label import gui_text_box, gui_label
from openpilot.system.ui.lib.wifi_manager import WifiManagerWrapper
from openpilot.system.ui.widgets.network import WifiManagerUI
# Constants # Constants
MARGIN = 50 MARGIN = 50
@ -39,6 +42,8 @@ class Updater:
self.show_reboot_button = False self.show_reboot_button = False
self.process = None self.process = None
self.update_thread = None self.update_thread = None
self.wifi_manager = WifiManagerWrapper()
self.wifi_manager_ui = WifiManagerUI(self.wifi_manager)
def install_update(self): def install_update(self):
self.current_screen = Screen.PROGRESS self.current_screen = Screen.PROGRESS
@ -79,8 +84,9 @@ class Updater:
gui_label(title_rect, "Update Required", TITLE_FONT_SIZE, font_weight=FontWeight.BOLD) gui_label(title_rect, "Update Required", TITLE_FONT_SIZE, font_weight=FontWeight.BOLD)
# Description # Description
desc_text = "An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. \ desc_text = ("An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. " +
The download size is approximately 1GB." "The download size is approximately 1GB.")
desc_rect = rl.Rectangle(MARGIN + 50, 250 + TITLE_FONT_SIZE + 75, gui_app.width - MARGIN * 2 - 100, BODY_FONT_SIZE * 3) desc_rect = rl.Rectangle(MARGIN + 50, 250 + TITLE_FONT_SIZE + 75, gui_app.width - MARGIN * 2 - 100, BODY_FONT_SIZE * 3)
gui_text_box(desc_rect, desc_text, BODY_FONT_SIZE) gui_text_box(desc_rect, desc_text, BODY_FONT_SIZE)
@ -101,49 +107,17 @@ class Updater:
return # Return to avoid further processing after action return # Return to avoid further processing after action
def render_wifi_screen(self): def render_wifi_screen(self):
# Title and back button # Draw the Wi-Fi manager UI
title_rect = rl.Rectangle(MARGIN + 50, MARGIN, gui_app.width - MARGIN * 2 - 100, 60) wifi_rect = rl.Rectangle(MARGIN + 50, MARGIN, gui_app.width - MARGIN * 2 - 100, gui_app.height - MARGIN * 2 - BUTTON_HEIGHT - 20)
gui_label(title_rect, "Wi-Fi Networks", 60, font_weight=FontWeight.BOLD) self.wifi_manager_ui.render(wifi_rect)
if self.wifi_manager_ui.require_full_screen:
return
back_button_rect = rl.Rectangle(MARGIN, gui_app.height - MARGIN - BUTTON_HEIGHT, BUTTON_WIDTH, BUTTON_HEIGHT) back_button_rect = rl.Rectangle(MARGIN, gui_app.height - MARGIN - BUTTON_HEIGHT, BUTTON_WIDTH, BUTTON_HEIGHT)
if gui_button(back_button_rect, "Back"): if gui_button(back_button_rect, "Back"):
self.current_screen = Screen.PROMPT self.current_screen = Screen.PROMPT
return # Return to avoid processing other interactions after screen change return # Return to avoid processing other interactions after screen change
# Draw placeholder for WiFi implementation
placeholder_rect = rl.Rectangle(
MARGIN,
title_rect.y + title_rect.height + MARGIN,
gui_app.width - MARGIN * 2,
gui_app.height - title_rect.height - MARGIN * 3 - BUTTON_HEIGHT
)
# Draw rounded rectangle background
rl.draw_rectangle_rounded(
placeholder_rect,
0.1,
10,
rl.Color(41, 41, 41, 255)
)
# Draw placeholder text
placeholder_text = "WiFi Implementation Placeholder"
text_size = rl.measure_text_ex(gui_app.font(), placeholder_text, 80, 1)
text_pos = rl.Vector2(
placeholder_rect.x + (placeholder_rect.width - text_size.x) / 2,
placeholder_rect.y + (placeholder_rect.height - text_size.y) / 2
)
rl.draw_text_ex(gui_app.font(), placeholder_text, text_pos, 80, 1, rl.WHITE)
# Draw instructions
instructions_text = "Real WiFi functionality would be implemented here"
instructions_size = rl.measure_text_ex(gui_app.font(), instructions_text, 40, 1)
instructions_pos = rl.Vector2(
placeholder_rect.x + (placeholder_rect.width - instructions_size.x) / 2,
text_pos.y + text_size.y + 20
)
rl.draw_text_ex(gui_app.font(), instructions_text, instructions_pos, 40, 1, rl.GRAY)
def render_progress_screen(self): def render_progress_screen(self):
title_rect = rl.Rectangle(MARGIN + 100, 330, gui_app.width - MARGIN * 2 - 200, 100) title_rect = rl.Rectangle(MARGIN + 100, 330, gui_app.width - MARGIN * 2 - 200, 100)
gui_label(title_rect, self.progress_text, 90, font_weight=FontWeight.SEMI_BOLD) gui_label(title_rect, self.progress_text, 90, font_weight=FontWeight.SEMI_BOLD)

@ -0,0 +1,87 @@
import pyray as rl
from msgq.visionipc import VisionIpcClient, VisionStreamType
from openpilot.system.ui.lib.application import gui_app
FRAME_FRAGMENT_SHADER = """
#version 330 core
in vec2 fragTexCoord; uniform sampler2D texture0, texture1; out vec4 fragColor;
void main() {
float y = texture(texture0, fragTexCoord).r;
vec2 uv = texture(texture1, fragTexCoord).ra - 0.5;
fragColor = vec4(y + 1.402*uv.y, y - 0.344*uv.x - 0.714*uv.y, y + 1.772*uv.x, 1.0);
}"""
class CameraView:
def __init__(self, name: str, stream_type: VisionStreamType):
self.client = VisionIpcClient(name, stream_type, False)
self.shader = rl.load_shader_from_memory(rl.ffi.NULL, FRAME_FRAGMENT_SHADER)
self.texture_y: rl.Texture | None = None
self.texture_uv: rl.Texture | None = None
self.frame = None
def close(self):
self._clear_textures()
if self.shader and self.shader.id:
rl.unload_shader(self.shader)
def render(self, rect: rl.Rectangle):
if not self._ensure_connection():
return
buffer = self.client.recv(timeout_ms=0)
self.frame = buffer if buffer else self.frame
if not self.frame or not self.texture_y or not self.texture_uv:
return
y_data = self.frame.data[: self.frame.uv_offset]
uv_data = self.frame.data[self.frame.uv_offset :]
rl.update_texture(self.texture_y, rl.ffi.cast("void *", y_data.ctypes.data))
rl.update_texture(self.texture_uv, rl.ffi.cast("void *", uv_data.ctypes.data))
# Calculate scaling to maintain aspect ratio
scale = min(rect.width / self.frame.width, rect.height / self.frame.height)
x_offset = rect.x + (rect.width - (self.frame.width * scale)) / 2
y_offset = rect.y + (rect.height - (self.frame.height * scale)) / 2
src_rect = rl.Rectangle(0, 0, float(self.frame.width), float(self.frame.height))
dst_rect = rl.Rectangle(x_offset, y_offset, self.frame.width * scale, self.frame.height * scale)
rl.begin_shader_mode(self.shader)
rl.set_shader_value_texture(self.shader, rl.get_shader_location(self.shader, "texture1"), self.texture_uv)
rl.draw_texture_pro(self.texture_y, src_rect, dst_rect, rl.Vector2(0, 0), 0.0, rl.WHITE)
rl.end_shader_mode()
def _ensure_connection(self) -> bool:
if not self.client.is_connected():
self.frame = None
if not self.client.connect(False) or not self.client.num_buffers:
return False
self._clear_textures()
self.texture_y = rl.load_texture_from_image(rl.Image(None, int(self.client.stride),
int(self.client.height), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE))
self.texture_uv = rl.load_texture_from_image(rl.Image(None, int(self.client.stride // 2),
int(self.client.height // 2), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA))
return True
def _clear_textures(self):
if self.texture_y and self.texture_y.id:
rl.unload_texture(self.texture_y)
if self.texture_uv and self.texture_uv.id:
rl.unload_texture(self.texture_uv)
if __name__ == "__main__":
gui_app.init_window("watch3")
road_camera_view = CameraView("camerad", VisionStreamType.VISION_STREAM_ROAD)
driver_camera_view = CameraView("camerad", VisionStreamType.VISION_STREAM_DRIVER)
wide_road_camera_view = CameraView("camerad", VisionStreamType.VISION_STREAM_WIDE_ROAD)
try:
for _ in gui_app.render():
road_camera_view.render(rl.Rectangle(gui_app.width // 4, 0, gui_app.width // 2, gui_app.height // 2))
driver_camera_view.render(rl.Rectangle(0, gui_app.height // 2, gui_app.width // 2, gui_app.height // 2))
wide_road_camera_view.render(rl.Rectangle(gui_app.width // 2, gui_app.height // 2, gui_app.width // 2, gui_app.height // 2))
finally:
road_camera_view.close()
driver_camera_view.close()
wide_road_camera_view.close()

@ -1,4 +1,5 @@
import pyray as rl import pyray as rl
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.button import gui_button, ButtonStyle from openpilot.system.ui.lib.button import gui_button, ButtonStyle
from openpilot.system.ui.lib.label import gui_text_box from openpilot.system.ui.lib.label import gui_text_box
@ -11,10 +12,9 @@ TEXT_AREA_HEIGHT_REDUCTION = 200
BACKGROUND_COLOR = rl.Color(27, 27, 27, 255) BACKGROUND_COLOR = rl.Color(27, 27, 27, 255)
def confirm_dialog(rect: rl.Rectangle, message: str, confirm_text: str, cancel_text: str = "Cancel") -> int: def confirm_dialog(message: str, confirm_text: str, cancel_text: str = "Cancel") -> int:
# Calculate dialog position and size, centered within the parent rectangle dialog_x = (gui_app.width - DIALOG_WIDTH) / 2
dialog_x = rect.x + (rect.width - DIALOG_WIDTH) / 2 dialog_y = (gui_app.height - DIALOG_HEIGHT) / 2
dialog_y = rect.y + (rect.height - DIALOG_HEIGHT) / 2
dialog_rect = rl.Rectangle(dialog_x, dialog_y, DIALOG_WIDTH, DIALOG_HEIGHT) dialog_rect = rl.Rectangle(dialog_x, dialog_y, DIALOG_WIDTH, DIALOG_HEIGHT)
# Calculate button positions at the bottom of the dialog # Calculate button positions at the bottom of the dialog
@ -27,13 +27,7 @@ def confirm_dialog(rect: rl.Rectangle, message: str, confirm_text: str, cancel_t
yes_button = rl.Rectangle(yes_button_x, button_y, button_width, BUTTON_HEIGHT) yes_button = rl.Rectangle(yes_button_x, button_y, button_width, BUTTON_HEIGHT)
# Draw the dialog background # Draw the dialog background
rl.draw_rectangle( rl.draw_rectangle_rec(dialog_rect, BACKGROUND_COLOR)
int(dialog_rect.x),
int(dialog_rect.y),
int(dialog_rect.width),
int(dialog_rect.height),
BACKGROUND_COLOR,
)
# Draw the message in the dialog, centered # Draw the message in the dialog, centered
text_rect = rl.Rectangle(dialog_rect.x, dialog_rect.y, dialog_rect.width, dialog_rect.height - TEXT_AREA_HEIGHT_REDUCTION) text_rect = rl.Rectangle(dialog_rect.x, dialog_rect.y, dialog_rect.width, dialog_rect.height - TEXT_AREA_HEIGHT_REDUCTION)

@ -1,8 +1,11 @@
import pyray as rl import pyray as rl
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.button import gui_button from openpilot.system.ui.lib.button import gui_button
from openpilot.system.ui.lib.inputbox import InputBox
from openpilot.system.ui.lib.label import gui_label from openpilot.system.ui.lib.label import gui_label
# Constants for special keys # Constants for special keys
CONTENT_MARGIN = 50
BACKSPACE_KEY = "<-" BACKSPACE_KEY = "<-"
ENTER_KEY = "Enter" ENTER_KEY = "Enter"
SPACE_KEY = " " SPACE_KEY = " "
@ -42,30 +45,29 @@ keyboard_layouts = {
class Keyboard: class Keyboard:
def __init__(self, max_text_size: int = 255): def __init__(self, max_text_size: int = 255, min_text_size: int = 0):
self._layout = keyboard_layouts["lowercase"] self._layout = keyboard_layouts["lowercase"]
self._max_text_size = max_text_size self._max_text_size = max_text_size
self._string_pointer = rl.ffi.new("char[]", max_text_size) self._min_text_size = min_text_size
self._input_text = "" self._input_box = InputBox(max_text_size)
self._clear()
@property @property
def text(self): def text(self):
result = rl.ffi.string(self._string_pointer).decode("utf-8") return self._input_box.text
self._clear()
return result
def render(self, rect, title, sub_title): def clear(self):
self._input_box.clear()
def render(self, title: str, sub_title: str):
rect = rl.Rectangle(CONTENT_MARGIN, CONTENT_MARGIN, gui_app.width - 2 * CONTENT_MARGIN, gui_app.height - 2 * CONTENT_MARGIN)
gui_label(rl.Rectangle(rect.x, rect.y, rect.width, 95), title, 90) gui_label(rl.Rectangle(rect.x, rect.y, rect.width, 95), title, 90)
gui_label(rl.Rectangle(rect.x, rect.y + 95, rect.width, 60), sub_title, 55, rl.GRAY) gui_label(rl.Rectangle(rect.x, rect.y + 95, rect.width, 60), sub_title, 55, rl.GRAY)
if gui_button(rl.Rectangle(rect.x + rect.width - 300, rect.y, 300, 100), "Cancel"): if gui_button(rl.Rectangle(rect.x + rect.width - 300, rect.y, 300, 100), "Cancel"):
self._clear() self.clear()
return 0 return 0
# Text box for input # Text box for input
self._sync_string_pointer() self._input_box.render(rl.Rectangle(rect.x, rect.y + 160, rect.width, 100))
rl.gui_text_box(rl.Rectangle(rect.x, rect.y + 160, rect.width, 100), self._string_pointer, self._max_text_size, True)
self._input_text = rl.ffi.string(self._string_pointer).decode("utf-8")
h_space, v_space = 15, 15 h_space, v_space = 15, 15
row_y_start = rect.y + 300 # Starting Y position for the first row row_y_start = rect.y + 300 # Starting Y position for the first row
key_height = (rect.height - 300 - 3 * v_space) / 4 key_height = (rect.height - 300 - 3 * v_space) / 4
@ -84,7 +86,8 @@ class Keyboard:
key_rect = rl.Rectangle(start_x, row_y_start + row * (key_height + v_space), new_width, key_height) key_rect = rl.Rectangle(start_x, row_y_start + row * (key_height + v_space), new_width, key_height)
start_x += new_width start_x += new_width
if gui_button(key_rect, key): is_enabled = key != ENTER_KEY or len(self._input_box.text) >= self._min_text_size
if gui_button(key_rect, key, is_enabled=is_enabled):
if key == ENTER_KEY: if key == ENTER_KEY:
return 1 return 1
else: else:
@ -101,18 +104,21 @@ class Keyboard:
self._layout = keyboard_layouts["numbers"] self._layout = keyboard_layouts["numbers"]
elif key == SYMBOL_KEY: elif key == SYMBOL_KEY:
self._layout = keyboard_layouts["specials"] self._layout = keyboard_layouts["specials"]
elif key == BACKSPACE_KEY and len(self._input_text) > 0: elif key == BACKSPACE_KEY:
self._input_text = self._input_text[:-1] self._input_box.delete_char_before_cursor()
elif key != BACKSPACE_KEY and len(self._input_text) < self._max_text_size: else:
self._input_text += key self._input_box.add_char_at_cursor(key)
def _clear(self):
self._input_text = '' if __name__ == "__main__":
self._string_pointer[0] = b'\0' gui_app.init_window("Keyboard")
keyboard = Keyboard(min_text_size=8)
def _sync_string_pointer(self): for _ in gui_app.render():
"""Sync the C-string pointer with the internal Python string.""" result = keyboard.render("Keyboard", "Type here")
encoded = self._input_text.encode("utf-8")[:self._max_text_size - 1] # Leave room for the null terminator if result == 1:
buffer = rl.ffi.buffer(self._string_pointer) print(f"You typed: {keyboard.text}")
buffer[:len(encoded)] = encoded gui_app.request_close()
self._string_pointer[len(encoded)] = b'\0' # Null terminator elif result == 0:
print("Canceled")
gui_app.request_close()
gui_app.close()

@ -11,6 +11,8 @@ from openpilot.system.ui.widgets.keyboard import Keyboard
from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog
NM_DEVICE_STATE_NEED_AUTH = 60 NM_DEVICE_STATE_NEED_AUTH = 60
MIN_PASSWORD_LENGTH = 8
MAX_PASSWORD_LENGTH = 64
ITEM_HEIGHT = 160 ITEM_HEIGHT = 160
@ -46,28 +48,34 @@ class WifiManagerUI:
self.state: UIState = StateIdle() self.state: UIState = StateIdle()
self.btn_width = 200 self.btn_width = 200
self.scroll_panel = GuiScrollPanel() self.scroll_panel = GuiScrollPanel()
self.keyboard = Keyboard() self.keyboard = Keyboard(max_text_size=MAX_PASSWORD_LENGTH, min_text_size=MIN_PASSWORD_LENGTH)
self._networks: list[NetworkInfo] = []
self.wifi_manager = wifi_manager self.wifi_manager = wifi_manager
self.wifi_manager.set_callbacks(WifiManagerCallbacks(self._on_need_auth, self._on_activated, self._on_forgotten)) self.wifi_manager.set_callbacks(WifiManagerCallbacks(self._on_need_auth, self._on_activated, self._on_forgotten, self._on_network_updated))
self.wifi_manager.start() self.wifi_manager.start()
self.wifi_manager.connect() self.wifi_manager.connect()
def render(self, rect: rl.Rectangle): def render(self, rect: rl.Rectangle):
if not self.wifi_manager.networks: if not self._networks:
gui_label(rect, "Scanning Wi-Fi networks...", 72, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) gui_label(rect, "Scanning Wi-Fi networks...", 72, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
return return
match self.state: match self.state:
case StateNeedsAuth(network): case StateNeedsAuth(network):
result = self.keyboard.render(rect, "Enter password", f"for {network.ssid}") result = self.keyboard.render("Enter password", f"for {network.ssid}")
if result == 1: if result == 1:
self.connect_to_network(network, self.keyboard.text) password = self.keyboard.text
self.keyboard.clear()
if len(password) >= MIN_PASSWORD_LENGTH:
self.connect_to_network(network, password)
elif result == 0: elif result == 0:
self.state = StateIdle() self.state = StateIdle()
case StateShowForgetConfirm(network): case StateShowForgetConfirm(network):
result = confirm_dialog(rect, f'Forget Wi-Fi Network "{network.ssid}"?', "Forget") result = confirm_dialog(f'Forget Wi-Fi Network "{network.ssid}"?', "Forget")
if result == 1: if result == 1:
self.forget_network(network) self.forget_network(network)
elif result == 0: elif result == 0:
@ -76,20 +84,25 @@ class WifiManagerUI:
case _: case _:
self._draw_network_list(rect) self._draw_network_list(rect)
@property
def require_full_screen(self) -> bool:
"""Check if the WiFi UI requires exclusive full-screen rendering."""
return isinstance(self.state, (StateNeedsAuth, StateShowForgetConfirm))
def _draw_network_list(self, rect: rl.Rectangle): def _draw_network_list(self, rect: rl.Rectangle):
content_rect = rl.Rectangle(rect.x, rect.y, rect.width, len(self.wifi_manager.networks) * ITEM_HEIGHT) content_rect = rl.Rectangle(rect.x, rect.y, rect.width, len(self._networks) * ITEM_HEIGHT)
offset = self.scroll_panel.handle_scroll(rect, content_rect) offset = self.scroll_panel.handle_scroll(rect, content_rect)
clicked = self.scroll_panel.is_click_valid() clicked = self.scroll_panel.is_click_valid()
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height)) rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
for i, network in enumerate(self.wifi_manager.networks): for i, network in enumerate(self._networks):
y_offset = rect.y + i * ITEM_HEIGHT + offset.y y_offset = rect.y + i * ITEM_HEIGHT + offset.y
item_rect = rl.Rectangle(rect.x, y_offset, rect.width, ITEM_HEIGHT) item_rect = rl.Rectangle(rect.x, y_offset, rect.width, ITEM_HEIGHT)
if not rl.check_collision_recs(item_rect, rect): if not rl.check_collision_recs(item_rect, rect):
continue continue
self._draw_network_item(item_rect, network, clicked) self._draw_network_item(item_rect, network, clicked)
if i < len(self.wifi_manager.networks) - 1: if i < len(self._networks) - 1:
line_y = int(item_rect.y + item_rect.height - 1) line_y = int(item_rect.y + item_rect.height - 1)
rl.draw_line(int(item_rect.x), int(line_y), int(item_rect.x + item_rect.width), line_y, rl.LIGHTGRAY) rl.draw_line(int(item_rect.x), int(line_y), int(item_rect.x + item_rect.width), line_y, rl.LIGHTGRAY)
@ -115,7 +128,7 @@ class WifiManagerUI:
rl.gui_label(state_rect, status_text) rl.gui_label(state_rect, status_text)
# If the network is saved, show the "Forget" button # If the network is saved, show the "Forget" button
if self.wifi_manager.is_saved(network.ssid): if network.is_saved:
forget_btn_rect = rl.Rectangle( forget_btn_rect = rl.Rectangle(
rect.x + rect.width - self.btn_width, rect.x + rect.width - self.btn_width,
rect.y + (ITEM_HEIGHT - 80) / 2, rect.y + (ITEM_HEIGHT - 80) / 2,
@ -126,22 +139,26 @@ class WifiManagerUI:
self.state = StateShowForgetConfirm(network) self.state = StateShowForgetConfirm(network)
if isinstance(self.state, StateIdle) and rl.check_collision_point_rec(rl.get_mouse_position(), label_rect) and clicked: if isinstance(self.state, StateIdle) and rl.check_collision_point_rec(rl.get_mouse_position(), label_rect) and clicked:
if not self.wifi_manager.is_saved(network.ssid): if not network.is_saved:
self.state = StateNeedsAuth(network) self.state = StateNeedsAuth(network)
else: else:
self.connect_to_network(network) self.connect_to_network(network)
def connect_to_network(self, network: NetworkInfo, password=''): def connect_to_network(self, network: NetworkInfo, password=''):
self.state = StateConnecting(network) self.state = StateConnecting(network)
if self.wifi_manager.is_saved(network.ssid) and not password: if network.is_saved and not password:
self.wifi_manager.activate_connection(network.ssid) self.wifi_manager.activate_connection(network.ssid)
else: else:
self.wifi_manager.connect_to_network(network.ssid, password) self.wifi_manager.connect_to_network(network.ssid, password)
def forget_network(self, network: NetworkInfo): def forget_network(self, network: NetworkInfo):
self.state = StateForgetting(network) self.state = StateForgetting(network)
network.is_saved = False
self.wifi_manager.forget_connection(network.ssid) self.wifi_manager.forget_connection(network.ssid)
def _on_network_updated(self, networks: list[NetworkInfo]):
self._networks = networks
def _on_need_auth(self, ssid): def _on_need_auth(self, ssid):
match self.state: match self.state:
case StateConnecting(ssid): case StateConnecting(ssid):

@ -0,0 +1,81 @@
import pyray as rl
from openpilot.system.ui.lib.button import gui_button, ButtonStyle, TextAlignment
from openpilot.system.ui.lib.label import gui_label
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
class MultiOptionDialog:
def __init__(self, title, options, current=""):
self._title = title
self._options = options
self._current = current if current in options else ""
self._selection = self._current
self._option_height = 80
self._padding = 20
self.scroll_panel = GuiScrollPanel()
@property
def selection(self):
return self._selection
def render(self, rect):
title_rect = rl.Rectangle(rect.x + self._padding, rect.y + self._padding, rect.width - 2 * self._padding, 70)
gui_label(title_rect, self._title, 70)
options_y_start = rect.y + 120
options_height = len(self._options) * (self._option_height + 10)
options_rect = rl.Rectangle(rect.x + self._padding, options_y_start, rect.width - 2 * self._padding, options_height)
view_rect = rl.Rectangle(
rect.x + self._padding, options_y_start, rect.width - 2 * self._padding, rect.height - 200 - 2 * self._padding
)
offset = self.scroll_panel.handle_scroll(view_rect, options_rect)
is_click_valid = self.scroll_panel.is_click_valid()
rl.begin_scissor_mode(int(view_rect.x), int(view_rect.y), int(view_rect.width), int(view_rect.height))
for i, option in enumerate(self._options):
y_pos = view_rect.y + i * (self._option_height + 10) + offset.y
item_rect = rl.Rectangle(view_rect.x, y_pos, view_rect.width, self._option_height)
if not rl.check_collision_recs(item_rect, view_rect):
continue
is_selected = option == self._selection
button_style = ButtonStyle.PRIMARY if is_selected else ButtonStyle.NORMAL
if gui_button(item_rect, option, button_style=button_style, text_alignment=TextAlignment.LEFT) and is_click_valid:
self._selection = option
rl.end_scissor_mode()
button_y = rect.y + rect.height - 80 - self._padding
button_width = (rect.width - 3 * self._padding) / 2
cancel_rect = rl.Rectangle(rect.x + self._padding, button_y, button_width, 80)
if gui_button(cancel_rect, "Cancel"):
return 0 # Canceled
select_rect = rl.Rectangle(rect.x + 2 * self._padding + button_width, button_y, button_width, 80)
has_new_selection = self._selection != "" and self._selection != self._current
if gui_button(select_rect, "Select", is_enabled=has_new_selection, button_style=ButtonStyle.PRIMARY):
return 1 # Selected
return -1 # Still active
if __name__ == "__main__":
from openpilot.system.ui.lib.application import gui_app
gui_app.init_window("Multi Option Dialog Example")
options = [f"Option {i}" for i in range(1, 11)]
dialog = MultiOptionDialog("Choose an option", options, options[0])
for _ in gui_app.render():
result = dialog.render(rl.Rectangle(100, 100, 1024, 800))
if result >= 0:
print(f"Selected: {dialog.selection}" if result > 0 else "Canceled")
break

@ -28,6 +28,7 @@ RESOLUTION = '2160x1080'
SECONDS_TO_WARM = 2 SECONDS_TO_WARM = 2
PROC_WAIT_SECONDS = 30 PROC_WAIT_SECONDS = 30
OPENPILOT_FONT = str(Path(BASEDIR, 'selfdrive/assets/fonts/Inter-Regular.ttf').resolve())
REPLAY = str(Path(BASEDIR, 'tools/replay/replay').resolve()) REPLAY = str(Path(BASEDIR, 'tools/replay/replay').resolve())
UI = str(Path(BASEDIR, 'selfdrive/ui/ui').resolve()) UI = str(Path(BASEDIR, 'selfdrive/ui/ui').resolve())
@ -165,17 +166,37 @@ def clip(data_dir: str | None, quality: Literal['low', 'high'], prefix: str, rou
# TODO: evaluate creating fn that inspects /tmp/.X11-unix and creates unused display to avoid possibility of collision # TODO: evaluate creating fn that inspects /tmp/.X11-unix and creates unused display to avoid possibility of collision
display = f':{randint(99, 999)}' display = f':{randint(99, 999)}'
box_style = 'box=1:boxcolor=black@0.33:boxborderw=7'
meta_text = get_meta_text(route) meta_text = get_meta_text(route)
overlays = [ overlays = [
f"drawtext=text='{escape_ffmpeg_text(meta_text)}':fontfile=Inter.tff:fontcolor=white:fontsize=18:box=1:boxcolor=black@0.33:boxborderw=7:x=(w-text_w)/2:y=5.5:enable='between(t,1,5)'" # metadata overlay
f"drawtext=text='{escape_ffmpeg_text(meta_text)}':fontfile={OPENPILOT_FONT}:fontcolor=white:fontsize=15:{box_style}:x=(w-text_w)/2:y=5.5:enable='between(t,1,5)'",
# route time overlay
f"drawtext=text='%{{eif\\:floor(({start}+t)/60)\\:d\\:2}}\\:%{{eif\\:mod({start}+t\\,60)\\:d\\:2}}':fontfile={OPENPILOT_FONT}:fontcolor=white:fontsize=24:{box_style}:x=w-text_w-38:y=38"
] ]
if title: if title:
overlays.append(f"drawtext=text='{escape_ffmpeg_text(title)}':fontfile=Inter.tff:fontcolor=white:fontsize=32:box=1:boxcolor=black@0.33:boxborderw=10:x=(w-text_w)/2:y=53") overlays.append(f"drawtext=text='{escape_ffmpeg_text(title)}':fontfile={OPENPILOT_FONT}:fontcolor=white:fontsize=32:{box_style}:x=(w-text_w)/2:y=53")
ffmpeg_cmd = [ ffmpeg_cmd = [
'ffmpeg', '-y', '-video_size', RESOLUTION, '-framerate', str(FRAMERATE), '-f', 'x11grab', '-draw_mouse', '0', 'ffmpeg', '-y',
'-i', display, '-c:v', 'libx264', '-maxrate', f'{bit_rate_kbps}k', '-bufsize', f'{bit_rate_kbps*2}k', '-crf', '23', '-video_size', RESOLUTION,
'-filter:v', ','.join(overlays), '-preset', 'ultrafast', '-pix_fmt', 'yuv420p', '-movflags', '+faststart', '-f', 'mp4', '-t', str(duration), out '-framerate', str(FRAMERATE),
'-f', 'x11grab',
'-rtbufsize', '100M',
'-draw_mouse', '0',
'-i', display,
'-c:v', 'libx264',
'-maxrate', f'{bit_rate_kbps}k',
'-bufsize', f'{bit_rate_kbps*2}k',
'-crf', '23',
'-filter:v', ','.join(overlays),
'-preset', 'ultrafast',
'-tune', 'zerolatency',
'-pix_fmt', 'yuv420p',
'-movflags', '+faststart',
'-f', 'mp4',
'-t', str(duration),
out,
] ]
replay_cmd = [REPLAY, '-c', '1', '-s', str(begin_at), '--prefix', prefix] replay_cmd = [REPLAY, '-c', '1', '-s', str(begin_at), '--prefix', prefix]

@ -33,7 +33,6 @@ function install_ubuntu_common_requirements() {
git \ git \
git-lfs \ git-lfs \
ffmpeg \ ffmpeg \
fonts-inter \
libavformat-dev \ libavformat-dev \
libavcodec-dev \ libavcodec-dev \
libavdevice-dev \ libavdevice-dev \

@ -254,7 +254,7 @@ function op_setup() {
function op_auth() { function op_auth() {
op_before_cmd op_before_cmd
op_run_command tools/lib/auth.py op_run_command tools/lib/auth.py "$@"
} }
function op_activate_venv() { function op_activate_venv() {

1316
uv.lock

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save