Merge branch 'upstream/openpilot/master' into sync-20250731

# Conflicts:
#	.github/workflows/selfdrive_tests.yaml
#	common/params.h
#	common/params_keys.h
#	common/params_pyx.pyx
#	docs/CARS.md
#	opendbc_repo
#	panda
#	selfdrive/car/tests/test_models.py
#	selfdrive/pandad/pandad.cc
#	selfdrive/pandad/pandad.h
#	selfdrive/selfdrived/selfdrived.py
#	selfdrive/ui/translations/main_ar.ts
#	selfdrive/ui/translations/main_de.ts
#	selfdrive/ui/translations/main_es.ts
#	selfdrive/ui/translations/main_fr.ts
#	selfdrive/ui/translations/main_ja.ts
#	selfdrive/ui/translations/main_ko.ts
#	selfdrive/ui/translations/main_pt-BR.ts
#	selfdrive/ui/translations/main_th.ts
#	selfdrive/ui/translations/main_tr.ts
#	selfdrive/ui/translations/main_zh-CHS.ts
#	selfdrive/ui/translations/main_zh-CHT.ts
#	system/athena/athenad.py
#	system/athena/manage_athenad.py
#	system/manager/manager.py
#	system/sentry.py
#	uv.lock
Sync: `commaai/opendbc:master` into `sunnypilot/opendbc:master`

Sync: `commaai/panda:master` into `sunnypilot/panda:master`
This commit is contained in:
Jason Wen
2025-08-01 12:26:50 -04:00
211 changed files with 4017 additions and 18401 deletions
+5
View File
@@ -8,3 +8,8 @@ Quick start:
* set `SCALE=1.5` to scale the entire UI by 1.5x
* https://www.raylib.com/cheatsheet/cheatsheet.html
* https://electronstudio.github.io/raylib-python-cffi/README.html#quickstart
Style guide:
* All graphical elements should subclass [`Widget`](/system/ui/widgets/__init__.py).
* Prefer a stateful widget over a function for easy migration from QT
* All internal class variables and functions should be prefixed with `_`
+16 -27
View File
@@ -7,7 +7,7 @@ import threading
from collections.abc import Callable
from collections import deque
from dataclasses import dataclass
from enum import IntEnum
from enum import StrEnum
from typing import NamedTuple
from importlib.resources import as_file, files
from openpilot.common.swaglog import cloudlog
@@ -33,16 +33,16 @@ ASSETS_DIR = files("openpilot.selfdrive").joinpath("assets")
FONT_DIR = ASSETS_DIR.joinpath("fonts")
class FontWeight(IntEnum):
THIN = 0
EXTRA_LIGHT = 1
LIGHT = 2
NORMAL = 3
MEDIUM = 4
SEMI_BOLD = 5
BOLD = 6
EXTRA_BOLD = 7
BLACK = 8
class FontWeight(StrEnum):
THIN = "Inter-Thin.ttf"
EXTRA_LIGHT = "Inter-ExtraLight.ttf"
LIGHT = "Inter-Light.ttf"
NORMAL = "Inter-Regular.ttf"
MEDIUM = "Inter-Medium.ttf"
SEMI_BOLD = "Inter-SemiBold.ttf"
BOLD = "Inter-Bold.ttf"
EXTRA_BOLD = "Inter-ExtraBold.ttf"
BLACK = "Inter-Black.ttf"
@dataclass
@@ -312,34 +312,23 @@ class GuiApplication:
return self._height
def _load_fonts(self):
font_files = (
"Inter-Thin.ttf",
"Inter-ExtraLight.ttf",
"Inter-Light.ttf",
"Inter-Regular.ttf",
"Inter-Medium.ttf",
"Inter-SemiBold.ttf",
"Inter-Bold.ttf",
"Inter-ExtraBold.ttf",
"Inter-Black.ttf",
)
# Create a character set from our keyboard layouts
from openpilot.system.ui.widgets.keyboard import KEYBOARD_LAYOUTS
all_chars = set()
for layout in KEYBOARD_LAYOUTS.values():
all_chars.update(key for row in layout for key in row)
all_chars = "".join(all_chars)
all_chars += "–✓°"
all_chars += "–✓×°"
codepoint_count = rl.ffi.new("int *", 1)
codepoints = rl.load_codepoints(all_chars, codepoint_count)
for index, font_file in enumerate(font_files):
with as_file(FONT_DIR.joinpath(font_file)) as fspath:
for font_weight_file in FontWeight:
with as_file(FONT_DIR.joinpath(font_weight_file)) as fspath:
font = rl.load_font_ex(fspath.as_posix(), 200, codepoints, codepoint_count[0])
rl.set_texture_filter(font.texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR)
self._fonts[index] = font
self._fonts[font_weight_file] = font
rl.unload_codepoints(codepoints)
rl.gui_set_font(self._fonts[FontWeight.NORMAL])
+1 -1
View File
@@ -91,7 +91,7 @@ class WifiManager:
# Set tethering ssid as "weedle" + first 4 characters of a dongle id
self._tethering_ssid = "weedle"
if Params is not None:
dongle_id = Params().get("DongleId", encoding="utf-8")
dongle_id = Params().get("DongleId")
if dongle_id:
self._tethering_ssid += "-" + dongle_id[:4]
self.running: bool = True
+43 -29
View File
@@ -1,18 +1,21 @@
#!/usr/bin/env python3
import os
import pyray as rl
import sys
import threading
import time
from enum import IntEnum
import pyray as rl
from openpilot.system.hardware import PC
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.label import gui_label, gui_text_box
from openpilot.system.ui.lib.widget import Widget
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import Button, ButtonStyle
from openpilot.system.ui.widgets.label import gui_label, gui_text_box
NVME = "/dev/nvme0n1"
USERDATA = "/dev/disk/by-partlabel/userdata"
TIMEOUT = 3*60
class ResetMode(IntEnum):
@@ -31,8 +34,16 @@ class ResetState(IntEnum):
class Reset(Widget):
def __init__(self, mode):
super().__init__()
self.mode = mode
self.reset_state = ResetState.NONE
self._mode = mode
self._previous_reset_state = None
self._reset_state = ResetState.NONE
self._cancel_button = Button("Cancel", self._cancel_callback)
self._confirm_button = Button("Confirm", self._confirm, button_style=ButtonStyle.PRIMARY)
self._reboot_button = Button("Reboot", lambda: os.system("sudo reboot"))
self._render_status = True
def _cancel_callback(self):
self._render_status = False
def _do_erase(self):
if PC:
@@ -50,53 +61,56 @@ class Reset(Widget):
if rm == 0 or fmt == 0:
os.system("sudo reboot")
else:
self.reset_state = ResetState.FAILED
self._reset_state = ResetState.FAILED
def start_reset(self):
self.reset_state = ResetState.RESETTING
self._reset_state = ResetState.RESETTING
threading.Timer(0.1, self._do_erase).start()
def _update_state(self):
if self._reset_state != self._previous_reset_state:
self._previous_reset_state = self._reset_state
self._timeout_st = time.monotonic()
elif self._reset_state != ResetState.RESETTING and (time.monotonic() - self._timeout_st) > TIMEOUT:
exit(0)
def _render(self, rect: rl.Rectangle):
label_rect = rl.Rectangle(rect.x + 140, rect.y, rect.width - 280, 100)
gui_label(label_rect, "System Reset", 100, font_weight=FontWeight.BOLD)
text_rect = rl.Rectangle(rect.x + 140, rect.y + 140, rect.width - 280, rect.height - 90 - 100)
gui_text_box(text_rect, self.get_body_text(), 90)
gui_text_box(text_rect, self._get_body_text(), 90)
button_height = 160
button_spacing = 50
button_top = rect.y + rect.height - button_height
button_width = (rect.width - button_spacing) / 2.0
if self.reset_state != ResetState.RESETTING:
if self.mode == ResetMode.RECOVER or self.reset_state == ResetState.FAILED:
if gui_button(rl.Rectangle(rect.x, button_top, button_width, button_height), "Reboot"):
os.system("sudo reboot")
elif self.mode == ResetMode.USER_RESET:
if gui_button(rl.Rectangle(rect.x, button_top, button_width, button_height), "Cancel"):
return False
if self._reset_state != ResetState.RESETTING:
if self._mode == ResetMode.RECOVER or self._reset_state == ResetState.FAILED:
self._reboot_button.render(rl.Rectangle(rect.x, button_top, rect.width, button_height))
elif self._mode == ResetMode.USER_RESET:
self._cancel_button.render(rl.Rectangle(rect.x, button_top, button_width, button_height))
if self.reset_state != ResetState.FAILED:
if gui_button(rl.Rectangle(rect.x + button_width + 50, button_top, button_width, button_height),
"Confirm", button_style=ButtonStyle.PRIMARY):
self.confirm()
if self._reset_state != ResetState.FAILED:
self._confirm_button.render(rl.Rectangle(rect.x + button_width + 50, button_top, button_width, button_height))
return True
return self._render_status
def confirm(self):
if self.reset_state == ResetState.CONFIRM:
def _confirm(self):
if self._reset_state == ResetState.CONFIRM:
self.start_reset()
else:
self.reset_state = ResetState.CONFIRM
self._reset_state = ResetState.CONFIRM
def get_body_text(self):
if self.reset_state == ResetState.CONFIRM:
def _get_body_text(self):
if self._reset_state == ResetState.CONFIRM:
return "Are you sure you want to reset your device?"
if self.reset_state == ResetState.RESETTING:
if self._reset_state == ResetState.RESETTING:
return "Resetting device...\nThis may take up to a minute."
if self.reset_state == ResetState.FAILED:
if self._reset_state == ResetState.FAILED:
return "Reset failed. Reboot to try again."
if self.mode == ResetMode.RECOVER:
if self._mode == ResetMode.RECOVER:
return "Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device."
return "System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot."
+4 -4
View File
@@ -10,11 +10,11 @@ import pyray as rl
from cereal import log
from openpilot.system.hardware import HARDWARE
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.label import gui_label, gui_text_box
from openpilot.system.ui.lib.widget import Widget
from openpilot.system.ui.widgets.network import WifiManagerUI, WifiManagerWrapper
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import gui_button, ButtonStyle
from openpilot.system.ui.widgets.keyboard import Keyboard
from openpilot.system.ui.widgets.label import gui_label, gui_text_box
from openpilot.system.ui.widgets.network import WifiManagerUI, WifiManagerWrapper
NetworkType = log.DeviceState.NetworkType
+1 -1
View File
@@ -5,8 +5,8 @@ import sys
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.widget import Widget
from openpilot.system.ui.text import wrap_text
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.sunnypilot.lib.application import gui_app_sp
+4 -4
View File
@@ -3,11 +3,11 @@ import re
import sys
import pyray as rl
from openpilot.system.hardware import HARDWARE, PC
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.button import gui_button, ButtonStyle
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.widget import Widget
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import gui_button, ButtonStyle
MARGIN = 50
SPACING = 40
+3 -3
View File
@@ -7,10 +7,10 @@ from enum import IntEnum
from openpilot.system.hardware import HARDWARE
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.label import gui_text_box, gui_label
from openpilot.system.ui.lib.wifi_manager import WifiManagerWrapper
from openpilot.system.ui.lib.widget import Widget
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import gui_button, ButtonStyle
from openpilot.system.ui.widgets.label import gui_text_box, gui_label
from openpilot.system.ui.widgets.network import WifiManagerUI
# Constants
@@ -1,7 +1,11 @@
import pyray as rl
from collections.abc import Callable
from enum import IntEnum
from openpilot.system.ui.lib.application import gui_app, FontWeight
import pyray as rl
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.widgets import Widget
class ButtonStyle(IntEnum):
@@ -54,6 +58,8 @@ BUTTON_PRESSED_BACKGROUND_COLORS = {
_pressed_buttons: set[str] = set() # Track mouse press state globally
# TODO: This should be a Widget class
def gui_button(
rect: rl.Rectangle,
text: str,
@@ -146,3 +152,44 @@ def gui_button(
rl.draw_text_ex(font, text, text_pos, font_size, 0, color)
return result
class Button(Widget):
def __init__(self,
text: str,
click_callback: Callable[[], None] = None,
font_size: int = DEFAULT_BUTTON_FONT_SIZE,
font_weight: FontWeight = FontWeight.MEDIUM,
button_style: ButtonStyle = ButtonStyle.NORMAL,
border_radius: int = 10,
):
super().__init__()
self._text = text
self._click_callback = click_callback
self._label_font = gui_app.font(FontWeight.SEMI_BOLD)
self._button_style = button_style
self._font_size = font_size
self._border_radius = border_radius
self._font_size = font_size
self._text_color = BUTTON_TEXT_COLOR[button_style]
self._text_size = measure_text_cached(gui_app.font(font_weight), text, font_size)
def _handle_mouse_release(self, mouse_pos: MousePos):
if self._click_callback:
print(f"Button clicked: {self._text}")
self._click_callback()
def _get_background_color(self) -> rl.Color:
if self._is_pressed:
return BUTTON_PRESSED_BACKGROUND_COLORS[self._button_style]
else:
return BUTTON_BACKGROUND_COLORS[self._button_style]
def _render(self, _):
roundness = self._border_radius / (min(self._rect.width, self._rect.height) / 2)
rl.draw_rectangle_rounded(self._rect, roundness, 10, self._get_background_color())
text_pos = rl.Vector2(0, self._rect.y + (self._rect.height - self._text_size.y) // 2)
text_pos.x = self._rect.x + (self._rect.width - self._text_size.x) // 2
rl.draw_text_ex(self._label_font, self._text, text_pos, self._font_size, 0, self._text_color)
+3 -3
View File
@@ -1,8 +1,8 @@
import pyray as rl
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.label import gui_text_box
from openpilot.system.ui.lib.widget import DialogResult
from openpilot.system.ui.widgets import DialogResult
from openpilot.system.ui.widgets.button import gui_button, ButtonStyle
from openpilot.system.ui.widgets.label import gui_text_box
DIALOG_WIDTH = 1520
DIALOG_HEIGHT = 600
+3 -3
View File
@@ -4,10 +4,10 @@ from dataclasses import dataclass
from enum import Enum
from typing import Any
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.wrap_text import wrap_text
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
from openpilot.system.ui.lib.widget import Widget, DialogResult
from openpilot.system.ui.lib.button import gui_button, ButtonStyle
from openpilot.system.ui.lib.wrap_text import wrap_text
from openpilot.system.ui.widgets import Widget, DialogResult
from openpilot.system.ui.widgets.button import gui_button, ButtonStyle
class ElementType(Enum):
@@ -2,7 +2,7 @@ import pyray as rl
import time
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.widget import Widget
from openpilot.system.ui.widgets import Widget
PASSWORD_MASK_CHAR = ""
PASSWORD_MASK_DELAY = 1.5 # Seconds to show character before masking
+4 -4
View File
@@ -2,10 +2,10 @@ import time
from typing import Literal
import pyray as rl
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.button import ButtonStyle, gui_button
from openpilot.system.ui.lib.inputbox import InputBox
from openpilot.system.ui.lib.label import gui_label
from openpilot.system.ui.lib.widget import Widget
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import ButtonStyle, gui_button
from openpilot.system.ui.widgets.inputbox import InputBox
from openpilot.system.ui.widgets.label import gui_label
KEY_FONT_SIZE = 96
DOUBLE_CLICK_THRESHOLD = 0.5 # seconds
@@ -4,6 +4,8 @@ from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.utils import GuiStyleContext
# TODO: This should be a Widget class
def gui_label(
rect: rl.Rectangle,
text: str,
@@ -5,9 +5,9 @@ from abc import ABC
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
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.button import gui_button, ButtonStyle
from openpilot.system.ui.lib.toggle import Toggle, WIDTH as TOGGLE_WIDTH, HEIGHT as TOGGLE_HEIGHT
from openpilot.system.ui.lib.widget import Widget
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import gui_button, ButtonStyle
from openpilot.system.ui.widgets.toggle import Toggle, WIDTH as TOGGLE_WIDTH, HEIGHT as TOGGLE_HEIGHT
ITEM_BASE_WIDTH = 600
ITEM_BASE_HEIGHT = 170
+4 -4
View File
@@ -4,13 +4,13 @@ from typing import Literal
import pyray as rl
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.button import ButtonStyle, gui_button
from openpilot.system.ui.lib.label import gui_label
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
from openpilot.system.ui.lib.wifi_manager import NetworkInfo, WifiManagerCallbacks, WifiManagerWrapper, SecurityType
from openpilot.system.ui.widgets.keyboard import Keyboard
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import ButtonStyle, gui_button
from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog
from openpilot.system.ui.lib.widget import Widget
from openpilot.system.ui.widgets.keyboard import Keyboard
from openpilot.system.ui.widgets.label import gui_label
NM_DEVICE_STATE_NEED_AUTH = 60
MIN_PASSWORD_LENGTH = 8
+3 -3
View File
@@ -1,9 +1,9 @@
import pyray as rl
from openpilot.system.ui.lib.application import FontWeight
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
from openpilot.system.ui.lib.widget import Widget
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import gui_button, ButtonStyle, TextAlignment
from openpilot.system.ui.widgets.label import gui_label
# Constants
MARGIN = 50
@@ -1,6 +1,6 @@
import pyray as rl
from openpilot.system.ui.lib.widget import Widget
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
from openpilot.system.ui.widgets import Widget
ITEM_SPACING = 40
LINE_COLOR = rl.GRAY
@@ -1,6 +1,6 @@
import pyray as rl
from openpilot.system.ui.lib.application import MousePos
from openpilot.system.ui.lib.widget import Widget
from openpilot.system.ui.widgets import Widget
ON_COLOR = rl.Color(51, 171, 76, 255)
OFF_COLOR = rl.Color(0x39, 0x39, 0x39, 255)