Compare commits

...

40 Commits

Author SHA1 Message Date
nayan
d57983c680 sunnylink panel 2025-11-22 21:55:44 -05:00
nayan
b3a9169a96 Merge branch 'rl-sunnylink-panel' into mici-playground 2025-11-22 14:45:57 -05:00
nayan
9e794892e3 Merge branch 'py-sunnylink' into mici-playground 2025-11-22 13:41:14 -05:00
nayan
abd19ca8da Merge branch 'py-ui-state-sp' into mici-playground 2025-11-22 13:41:11 -05:00
nayan
8fa22cd14c init sunnylink panel 2025-11-22 12:35:33 -05:00
nayan
471621ff84 Merge branch 'py-ui-state-sp' into rl-sunnylink-panel 2025-11-21 18:15:37 -05:00
nayan
3f664201a6 Merge branch 'rl-sp-panels' into rl-sunnylink-panel 2025-11-21 18:15:13 -05:00
nayan
75a3591cd6 Merge remote-tracking branch 'origin/ui-gui-app-ext' into rl-sunnylink-panel 2025-11-21 18:14:44 -05:00
nayan
5d3f95d420 use gui_app.sunnypilot_ui() 2025-11-21 18:08:19 -05:00
nayan
f8d19fe9dd Merge branch 'rl-sp-toggles' into rl-sp-panels 2025-11-21 18:07:19 -05:00
nayan
9d711350c2 Merge remote-tracking branch 'origin/ui-gui-app-ext' into rl-sp-panels 2025-11-21 18:07:03 -05:00
nayan
45c853c87a Merge remote-tracking branch 'origin/ui-gui-app-ext' into py-ui-state-sp 2025-11-21 17:57:35 -05:00
nayan
e8ab9d812d use gui_app.sunnypilot_ui() 2025-11-21 17:57:06 -05:00
nayan
e74252bdf5 Merge remote-tracking branch 'origin/master' into py-ui-state-sp 2025-11-21 15:37:33 -05:00
nayan
b5a525b280 Merge branch 'rl-sp-panels' into rl-sunnylink-panel
# Conflicts:
#	selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py
2025-11-21 15:28:50 -05:00
nayan
48d33e98e7 scroller -> scroller_tici 2025-11-21 15:27:51 -05:00
nayan
009d350a58 Merge branch 'rl-sp-panels' into rl-sunnylink-panel 2025-11-21 15:26:06 -05:00
nayan
49e441dd1a Merge remote-tracking branch 'origin/master' into rl-sunnylink-panel 2025-11-21 15:25:58 -05:00
nayan
2717d97350 scroller -> scroller_tici 2025-11-21 15:20:31 -05:00
nayan
64232397ed Merge remote-tracking branch 'origin/master' into rl-sp-panels 2025-11-21 15:16:50 -05:00
nayan
ffa78eabaa Revert "add ui_update callback"
This reverts commit 4da32cc009.
2025-11-20 17:58:19 -05:00
nayan
8c3cf8e542 better 2025-11-20 10:54:46 -05:00
nayan
bf673a38c8 change it up 2025-11-19 23:29:06 -05:00
nayan
4da32cc009 add ui_update callback 2025-11-19 23:03:56 -05:00
nayan
47687d4664 not needed 2025-11-19 22:58:05 -05:00
nayan
b7d1d82e1e handle layout updates 2025-11-19 22:50:56 -05:00
nayan
63fe9d8f9c enable, disable, enable, disable 2025-11-19 22:03:30 -05:00
nayan
ddb71f81d7 update 2025-11-19 21:57:36 -05:00
nayan
b65c550cc8 fruit loops 2025-11-19 21:12:06 -05:00
nayan
6b45d2648b backup & restore 2025-11-19 14:02:58 -05:00
nayan
a8b319cfff init panel elements 2025-11-19 13:01:20 -05:00
nayan
6b1da28c01 sponsor & pairing qr 2025-11-19 13:01:09 -05:00
nayan
d3d812dd92 Merge branch 'refs/heads/py-sunnylink' into rl-sunnylink-panel 2025-11-19 12:28:09 -05:00
nayan
b670c6a056 Merge branch 'refs/heads/py-ui-state-sp' into rl-sunnylink-panel 2025-11-19 12:26:00 -05:00
nayan
a9e57f0a76 add ui previews 2025-11-16 12:47:37 -05:00
nayan
712a358c94 Merge branch 'rl-sp-toggles' into rl-sp-panels 2025-11-16 11:23:31 -05:00
nayan
632e9d13b2 Merge branch 'rl-sp-toggles' into rl-sp-panels 2025-11-16 09:55:25 -05:00
nayan
51d0666c85 more patience, grasshopper 2025-11-16 09:33:35 -05:00
nayan
deda1329a2 patience, grasshopper 2025-11-16 09:33:35 -05:00
nayan
4110749cb0 Panels. With Icons. And Scroller. 2025-11-16 09:33:35 -05:00
23 changed files with 1106 additions and 40 deletions

View File

@@ -10,6 +10,8 @@ from openpilot.selfdrive.ui.ui_state import device, ui_state
from openpilot.system.ui.widgets import Widget
from openpilot.selfdrive.ui.layouts.onboarding import OnboardingWindow
if gui_app.sunnypilot_ui():
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.settings import SettingsLayoutSP as SettingsLayout
class MainState(IntEnum):
HOME = 0

View File

@@ -118,6 +118,9 @@ class MiciHomeLayout(Widget):
self._branch_label = MiciLabel("", font_size=36, color=rl.GRAY, font_weight=FontWeight.ROMAN, elide_right=False, scroll=True)
self._version_commit_label = MiciLabel("", font_size=36, color=rl.GRAY, font_weight=FontWeight.ROMAN)
if gui_app.sunnypilot_ui():
self._openpilot_label.set_text("sunnypilot")
def show_event(self):
self._version_text = self._get_version_text()
self._update_network_status(ui_state.sm['deviceState'])

View File

@@ -2,7 +2,7 @@ import pyray as rl
from enum import IntEnum
import cereal.messaging as messaging
from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout
from openpilot.selfdrive.ui.mici.layouts.settings.settings import SettingsLayout
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.settings import SettingsLayoutSP as SettingsLayout
from openpilot.selfdrive.ui.mici.layouts.offroad_alerts import MiciOffroadAlerts
from openpilot.selfdrive.ui.mici.onroad.augmented_road_view import AugmentedRoadView
from openpilot.selfdrive.ui.ui_state import device, ui_state

View File

@@ -0,0 +1,24 @@
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.widgets import Widget
from openpilot.common.params import Params
class CruiseLayout(Widget):
def __init__(self):
super().__init__()
self._params = Params()
items = self._initialize_items()
self._scroller = Scroller(items, line_separator=True, spacing=0)
def _initialize_items(self):
items = [
]
return items
def _render(self, rect):
self._scroller.render(rect)
def show_event(self):
self._scroller.show_event()

View File

@@ -0,0 +1,5 @@
from openpilot.selfdrive.ui.layouts.settings.device import DeviceLayout
class DeviceLayoutSP(DeviceLayout):
def __init__(self):
DeviceLayout.__init__(self)

View File

@@ -0,0 +1,24 @@
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.widgets import Widget
from openpilot.common.params import Params
class DisplayLayout(Widget):
def __init__(self):
super().__init__()
self._params = Params()
items = self._initialize_items()
self._scroller = Scroller(items, line_separator=True, spacing=0)
def _initialize_items(self):
items = [
]
return items
def _render(self, rect):
self._scroller.render(rect)
def show_event(self):
self._scroller.show_event()

View File

@@ -0,0 +1,24 @@
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.widgets import Widget
from openpilot.common.params import Params
class ModelsLayout(Widget):
def __init__(self):
super().__init__()
self._params = Params()
items = self._initialize_items()
self._scroller = Scroller(items, line_separator=True, spacing=0)
def _initialize_items(self):
items = [
]
return items
def _render(self, rect):
self._scroller.render(rect)
def show_event(self):
self._scroller.show_event()

View File

@@ -0,0 +1,24 @@
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.widgets import Widget
from openpilot.common.params import Params
class NavigationLayout(Widget):
def __init__(self):
super().__init__()
self._params = Params()
items = self._initialize_items()
self._scroller = Scroller(items, line_separator=True, spacing=0)
def _initialize_items(self):
items = [
]
return items
def _render(self, rect):
self._scroller.render(rect)
def show_event(self):
self._scroller.show_event()

View File

@@ -0,0 +1,24 @@
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.widgets import Widget
from openpilot.common.params import Params
class OSMLayout(Widget):
def __init__(self):
super().__init__()
self._params = Params()
items = self._initialize_items()
self._scroller = Scroller(items, line_separator=True, spacing=0)
def _initialize_items(self):
items = [
]
return items
def _render(self, rect):
self._scroller.render(rect)
def show_event(self):
self._scroller.show_event()

View File

@@ -0,0 +1,189 @@
import pyray as rl
from dataclasses import dataclass
from enum import IntEnum
from openpilot.selfdrive.ui.layouts.settings import settings as OP
from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.device import DeviceLayoutSP
from openpilot.selfdrive.ui.layouts.settings.firehose import FirehoseLayout
from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout
from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout
from openpilot.system.ui.lib.application import gui_app,MousePos
from openpilot.system.ui.lib.multilang import tr_noop
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.widgets.network import NetworkUI
from openpilot.system.ui.lib.wifi_manager import WifiManager
from openpilot.system.ui.widgets import Widget
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.models import ModelsLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.sunnylink import SunnylinkLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.osm import OSMLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.trips import TripsLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle import VehicleLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.steering import SteeringLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.cruise import CruiseLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.visuals import VisualsLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.display import DisplayLayout
#from openpilot.selfdrive.ui.sunnypilot.layouts.settings.navigation import NavigationLayout
OP.PANEL_COLOR = rl.Color(10, 10, 10, 255)
ICON_SIZE = 70
OP.PanelType = IntEnum( # type: ignore
"PanelType",
[es.name for es in OP.PanelType] + [
"SUNNYLINK",
"MODELS",
"STEERING",
"CRUISE",
"VISUALS",
"DISPLAY",
"OSM",
"NAVIGATION",
"TRIPS",
"VEHICLE",
],
start=0,
)
@dataclass
class PanelInfo(OP.PanelInfo):
icon: str = ""
class SettingsLayoutSP(OP.SettingsLayout):
def __init__(self):
OP.SettingsLayout.__init__(self)
self._nav_items: list[Widget] = []
# Create sidebar scroller
self._sidebar_scroller = Scroller([], spacing=0, line_separator = False, pad_end=False)
# Panel configuration
wifi_manager = WifiManager()
wifi_manager.set_active(False)
self._panels = {
OP.PanelType.DEVICE: PanelInfo(tr_noop("Device"), DeviceLayoutSP(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_home.png"),
OP.PanelType.NETWORK: PanelInfo(tr_noop("Network"), NetworkUI(wifi_manager), icon="icons/network.png"),
OP.PanelType.SUNNYLINK: PanelInfo(tr_noop("sunnylink"), SunnylinkLayout(), icon="icons/shell.png"),
OP.PanelType.TOGGLES: PanelInfo(tr_noop("Toggles"), TogglesLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_toggle.png"),
OP.PanelType.SOFTWARE: PanelInfo(tr_noop("Software"), SoftwareLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_software.png"),
OP.PanelType.MODELS: PanelInfo(tr_noop("Models"), ModelsLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_models.png"),
OP.PanelType.STEERING: PanelInfo(tr_noop("Steering"), SteeringLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_lateral.png"),
OP.PanelType.CRUISE: PanelInfo(tr_noop("Cruise"), CruiseLayout(), icon="icons/speed_limit.png"),
OP.PanelType.VISUALS: PanelInfo(tr_noop("Visuals"), VisualsLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_visuals.png"),
OP.PanelType.DISPLAY: PanelInfo(tr_noop("Display"), DisplayLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_display.png"),
OP.PanelType.OSM: PanelInfo(tr_noop("OSM"), OSMLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_map.png"),
#OP.PanelType.NAVIGATION: PanelInfo(tr_noop("Navigation"), NavigationLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_map.png"),
OP.PanelType.TRIPS: PanelInfo(tr_noop("Trips"), TripsLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_trips.png"),
OP.PanelType.VEHICLE: PanelInfo(tr_noop("Vehicle"), VehicleLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_vehicle.png"),
OP.PanelType.FIREHOSE: PanelInfo(tr_noop("Firehose"), FirehoseLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_firehose.png"),
OP.PanelType.DEVELOPER: PanelInfo(tr_noop("Developer"), DeveloperLayout(), icon="icons/shell.png"),
}
def _create_nav_button(self, panel_type: OP.PanelType, panel_info: PanelInfo) -> Widget:
class NavButton(Widget):
def __init__(self, parent, p_type, p_info):
super().__init__()
self.parent = parent
self.panel_type = p_type
self.panel_info = p_info
def _render(self, rect):
is_selected = self.panel_type == self.parent._current_panel
text_color = OP.TEXT_SELECTED if is_selected else OP.TEXT_NORMAL
content_x = rect.x + 90
text_size = measure_text_cached(self.parent._font_medium, self.panel_info.name, 65)
# Draw background if selected
if is_selected:
self.container_rect = rl.Rectangle(
content_x - 20, rect.y, OP.SIDEBAR_WIDTH - 70, OP.NAV_BTN_HEIGHT
)
rl.draw_rectangle_rounded(self.container_rect, 0.2, 5, OP.CLOSE_BTN_COLOR)
if self.panel_info.icon:
icon_texture = gui_app.texture(self.panel_info.icon, ICON_SIZE, ICON_SIZE, keep_aspect_ratio=True)
rl.draw_texture(icon_texture, int(content_x), int(rect.y + (OP.NAV_BTN_HEIGHT - icon_texture.height) / 2), rl.WHITE)
content_x += ICON_SIZE + 20
# Draw button text (right-aligned)
text_pos = rl.Vector2(
content_x,
rect.y + (OP.NAV_BTN_HEIGHT - text_size.y) / 2
)
rl.draw_text_ex(self.parent._font_medium, self.panel_info.name, text_pos, 55, 0, text_color)
# Store button rect for click detection
self.panel_info.button_rect = rect
return NavButton(self, panel_type, panel_info)
def _draw_sidebar(self, rect: rl.Rectangle):
rl.draw_rectangle_rec(rect, OP.SIDEBAR_COLOR)
# Close button
close_btn_rect = rl.Rectangle(
rect.x + (rect.width - OP.CLOSE_BTN_SIZE) / 2, rect.y + 60, OP.CLOSE_BTN_SIZE, OP.CLOSE_BTN_SIZE
)
pressed = (rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and
rl.check_collision_point_rec(rl.get_mouse_position(), close_btn_rect))
close_color = OP.CLOSE_BTN_PRESSED if pressed else OP.CLOSE_BTN_COLOR
rl.draw_rectangle_rounded(close_btn_rect, 1.0, 20, close_color)
icon_color = rl.Color(255, 255, 255, 255) if not pressed else rl.Color(220, 220, 220, 255)
icon_dest = rl.Rectangle(
close_btn_rect.x + (close_btn_rect.width - self._close_icon.width) / 2,
close_btn_rect.y + (close_btn_rect.height - self._close_icon.height) / 2,
self._close_icon.width,
self._close_icon.height,
)
rl.draw_texture_pro(
self._close_icon,
rl.Rectangle(0, 0, self._close_icon.width, self._close_icon.height),
icon_dest,
rl.Vector2(0, 0),
0,
icon_color,
)
# Store close button rect for click detection
self._close_btn_rect = close_btn_rect
# Navigation buttons with scroller
if not self._nav_items:
for panel_type, panel_info in self._panels.items():
nav_button = self._create_nav_button(panel_type, panel_info)
nav_button.rect.width = rect.width - 100 # Full width minus padding
nav_button.rect.height = OP.NAV_BTN_HEIGHT
self._nav_items.append(nav_button)
self._sidebar_scroller.add_widget(nav_button)
# Draw navigation section with scroller
nav_rect = rl.Rectangle(
rect.x,
rect.y + 300, # Starting Y position for nav items
rect.width,
rect.height - 300 # Remaining height after close button
)
if self._nav_items:
self._sidebar_scroller.render(nav_rect)
return
def _handle_mouse_release(self, mouse_pos: MousePos) -> bool:
# Check close button
if rl.check_collision_point_rec(mouse_pos, self._close_btn_rect):
if self._close_callback:
self._close_callback()
return True
# Check navigation buttons
for panel_type, panel_info in self._panels.items():
if rl.check_collision_point_rec(mouse_pos, panel_info.button_rect) and self._sidebar_scroller.scroll_panel.is_touch_valid():
self.set_current_panel(panel_type)
return True
return False

View File

@@ -0,0 +1,24 @@
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.widgets import Widget
from openpilot.common.params import Params
class SteeringLayout(Widget):
def __init__(self):
super().__init__()
self._params = Params()
items = self._initialize_items()
self._scroller = Scroller(items, line_separator=True, spacing=0)
def _initialize_items(self):
items = [
]
return items
def _render(self, rect):
self._scroller.render(rect)
def show_event(self):
self._scroller.show_event()

View File

@@ -0,0 +1,181 @@
from cereal import custom
from openpilot.selfdrive.ui.sunnypilot.ui_state import ui_state_sp
from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.sunnypilot.widgets.sunnylink_pairing_dialog import SunnylinkPairingDialog
from openpilot.system.ui.widgets.button import ButtonStyle, Button
from openpilot.system.ui.widgets.confirm_dialog import alert_dialog, ConfirmDialog
from openpilot.system.ui.widgets.list_view import button_item, dual_button_item
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.widgets import Widget, DialogResult
from openpilot.common.params import Params
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
class SunnylinkLayout(Widget):
def __init__(self):
super().__init__()
self._params = Params()
self._sunnylink_pairing_dialog: SunnylinkPairingDialog | None = None
self._restore_in_progress = False
self._backup_in_progress = False
self._sunnylink_enabled = self._params.get("SunnylinkEnabled")
items = self._initialize_items()
self._scroller = Scroller(items, line_separator=True, spacing=0)
def _initialize_items(self):
self._sunnylink_toggle = toggle_item_sp(
title=tr("Enable sunnylink"),
description=tr(
"This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that."),
param="SunnylinkEnabled"
)
self._sponsor_btn = button_item(
title=tr("Sponsor Status"),
button_text=tr("SPONSOR"),
description=tr(
"Become a sponsor of sunnypilot to get early access to sunnylink features when they become available."),
callback=lambda: self._handle_pair_btn(False)
)
self._pair_btn = button_item(
title=tr("Pair GitHub Account"),
button_text=tr("Not Paired"),
description=tr(
"Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink."),
callback=lambda: self._handle_pair_btn(True)
)
self._sunnylink_uploader_toggle = toggle_item_sp(
title=tr("Enable sunnylink uploader (infrastructure test)"),
description=tr("Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers.") +
tr(" (Only for highest tiers, and does NOT bring ANY benefit to you yet. We are just testing data volume.)"),
param="EnableSunnylinkUploader"
)
self._sunnylink_backup_restore_buttons = dual_button_item(
description="",
left_text=tr("Backup Settings"),
right_text=tr("Restore Settings"),
left_callback=self._handle_backup_btn,
right_callback=self._handle_restore_btn
)
self._backup_btn: Button = self._sunnylink_backup_restore_buttons.action_item.left_button # store for easy individual access
self._restore_btn: Button = self._sunnylink_backup_restore_buttons.action_item.right_button
self._backup_btn.set_button_style(ButtonStyle.NORMAL)
self._restore_btn.set_button_style(ButtonStyle.PRIMARY)
items = [
self._sunnylink_toggle,
self._sponsor_btn,
self._pair_btn,
self._sunnylink_uploader_toggle,
self._sunnylink_backup_restore_buttons
]
return items
def _handle_pair_btn(self, sponsor_pairing: bool = False):
sunnylink_dongle_id = self._params.get("SunnylinkDongleId") or UNREGISTERED_SUNNYLINK_DONGLE_ID
if sunnylink_dongle_id == UNREGISTERED_SUNNYLINK_DONGLE_ID:
gui_app.set_modal_overlay(alert_dialog(message=tr("sunnylink Dongle ID not found. ") +
tr("This may be due to weak internet connection or sunnylink registration issue. ") +
tr("Please reboot and try again.")))
elif not self._sunnylink_pairing_dialog:
self._sunnylink_pairing_dialog = SunnylinkPairingDialog(sponsor_pairing)
gui_app.set_modal_overlay(self._sunnylink_pairing_dialog, callback=lambda result: setattr(self, '_sunnylink_pairing_dialog', None))
def _handle_backup_btn(self):
gui_app.set_modal_overlay(alert_dialog(message=tr("Backup functionality is currently not available.") +
tr(" It will be back once all settings are implemented on raylib.")))
# backup_dialog = ConfirmDialog(text=tr("Are you sure you want to backup your current sunnypilot settings?"), confirm_text="Backup")
# gui_app.set_modal_overlay(backup_dialog, callback=self._backup_handler)
def _handle_restore_btn(self):
self._restore_btn.set_enabled(False)
restore_dialog = ConfirmDialog(text=tr("Are you sure you want to restore the last backed up sunnypilot settings?"), confirm_text="Restore")
gui_app.set_modal_overlay(restore_dialog, callback=self._restore_handler)
def _backup_handler(self, dialog_result: int):
if dialog_result == DialogResult.CONFIRM:
self._backup_in_progress = True
self._backup_btn.set_enabled(False)
self._params.put_bool("BackupManager_CreateBackup", True)
def _restore_handler(self, dialog_result: int):
if dialog_result == DialogResult.CONFIRM:
self._restore_in_progress = True
self._restore_btn.set_enabled(False)
self._params.put("BackupManager_RestoreVersion", "latest")
def handle_backup_restore_progress(self):
sunnylink_backup_manager = ui_state_sp.sm["backupManagerSP"]
backup_status = sunnylink_backup_manager.backupStatus
restore_status = sunnylink_backup_manager.restoreStatus
backup_progress = sunnylink_backup_manager.backupProgress
restore_progress = sunnylink_backup_manager.restoreProgress
if self._backup_in_progress:
if backup_status == custom.BackupManagerSP.Status.inProgress:
text = tr(f"Backing up {backup_progress}%")
self._backup_btn.set_text(text)
elif backup_status == custom.BackupManagerSP.Status.failed:
self._backup_in_progress = False
self._backup_btn.set_enabled(not ui_state_sp.is_onroad())
self._backup_btn.set_text(tr("Backup Failed"))
elif backup_status == custom.BackupManagerSP.Status.completed:
self._backup_in_progress = False
dialog = alert_dialog(tr("Settings backup completed."))
gui_app.set_modal_overlay(dialog)
self._backup_btn.set_enabled(not ui_state_sp.is_onroad())
elif self._restore_in_progress:
if restore_status == custom.BackupManagerSP.Status.inProgress:
self._restore_btn.set_enabled(False)
text = tr(f"Restoring {restore_progress}%")
self._restore_btn.set_text(text)
elif restore_status == custom.BackupManagerSP.Status.failed:
self._restore_in_progress = False
self._restore_btn.set_enabled(not ui_state_sp.is_onroad())
self._restore_btn.set_text(tr("Restore Failed"))
dialog = alert_dialog(tr("Unable to restore the settings, try again later."))
gui_app.set_modal_overlay(dialog)
elif restore_status == custom.BackupManagerSP.Status.completed:
self._restore_in_progress = False
dialog = alert_dialog(tr("Settings restored. Confirm to restart the interface."))
gui_app.set_modal_overlay(dialog, callback=lambda: {
gui_app.request_close()
})
else:
can_enable = self._sunnylink_enabled and not ui_state_sp.is_onroad()
self._backup_btn.set_enabled(can_enable)
self._backup_btn.set_text(tr("Backup Settings"))
self._restore_btn.set_enabled(can_enable)
self._restore_btn.set_text(tr("Restore Settings"))
def _update_state(self):
super()._update_state()
self._sunnylink_enabled = self._params.get("SunnylinkEnabled")
self._sunnylink_toggle.action_item.set_enabled(not ui_state_sp.is_onroad())
self._sunnylink_toggle.action_item.set_state(self._sunnylink_enabled)
self._sunnylink_uploader_toggle.action_item.set_enabled(self._sunnylink_enabled)
self.handle_backup_restore_progress()
sponsor_btn_text = tr("THANKS ♥") if ui_state_sp.sunnylink_state.is_sponsor() else tr("SPONSOR")
self._sponsor_btn.action_item.set_text(sponsor_btn_text)
self._sponsor_btn.action_item.set_enabled(self._sunnylink_enabled)
pair_btn_text = tr("Paired") if ui_state_sp.sunnylink_state.is_paired() else tr("Not Paired")
self._pair_btn.action_item.set_text(pair_btn_text)
self._pair_btn.action_item.set_enabled(self._sunnylink_enabled)
def _render(self, rect):
self._scroller.render(rect)
def show_event(self):
self._scroller.show_event()

View File

@@ -0,0 +1,24 @@
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.widgets import Widget
from openpilot.common.params import Params
class TripsLayout(Widget):
def __init__(self):
super().__init__()
self._params = Params()
items = self._initialize_items()
self._scroller = Scroller(items, line_separator=True, spacing=0)
def _initialize_items(self):
items = [
]
return items
def _render(self, rect):
self._scroller.render(rect)
def show_event(self):
self._scroller.show_event()

View File

@@ -0,0 +1,24 @@
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.widgets import Widget
from openpilot.common.params import Params
class VehicleLayout(Widget):
def __init__(self):
super().__init__()
self._params = Params()
items = self._initialize_items()
self._scroller = Scroller(items, line_separator=True, spacing=0)
def _initialize_items(self):
items = [
]
return items
def _render(self, rect):
self._scroller.render(rect)
def show_event(self):
self._scroller.show_event()

View File

@@ -0,0 +1,24 @@
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.widgets import Widget
from openpilot.common.params import Params
class VisualsLayout(Widget):
def __init__(self):
super().__init__()
self._params = Params()
items = self._initialize_items()
self._scroller = Scroller(items, line_separator=True, spacing=0)
def _initialize_items(self):
items = [
]
return items
def _render(self, rect):
self._scroller.render(rect)
def show_event(self):
self._scroller.show_event()

View File

@@ -0,0 +1,40 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from enum import IntEnum
import pyray as rl
from openpilot.selfdrive.ui.mici.layouts.settings import settings as OP
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.sunnylink import SunnylinkLayoutMici
OP.PANEL_COLOR = rl.Color(10, 10, 10, 255)
ICON_SIZE = 70
OP.PanelType = IntEnum( # type: ignore
"PanelType",
[es.name for es in OP.PanelType] + [
"SUNNYLINK",
],
start=0,
)
class SettingsLayoutSP(OP.SettingsLayout):
def __init__(self):
OP.SettingsLayout.__init__(self)
sunnylink_btn = BigButton("sunnylink", "", "icons_mici/settings/developer/ssh.png")
sunnylink_btn.set_click_callback(lambda: self._set_current_panel(OP.PanelType.SUNNYLINK))
self._panels.update({
OP.PanelType.SUNNYLINK: OP.PanelInfo("sunnylink", SunnylinkLayoutMici(back_callback=lambda: self._set_current_panel(None))),
})
items = self._scroller._items.copy()
items.insert(1, sunnylink_btn)
self._scroller._items.clear()
for item in items:
self._scroller.add_widget(item)

View File

@@ -0,0 +1,170 @@
import pyray as rl
from collections.abc import Callable
from cereal import log, custom
from openpilot.common.params_pyx import Params
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialogV2
from openpilot.selfdrive.ui.sunnypilot.mici.widgets.sunnylink_pairing_dialog import SunnylinkPairingDialog
from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.selfdrive.ui.mici.widgets.button import BigParamControl, BigButton, BigToggle
from openpilot.system.ui.lib.application import gui_app, MousePos
from openpilot.system.ui.widgets import NavWidget
from openpilot.selfdrive.ui.layouts.settings.common import restart_needed_callback
from openpilot.selfdrive.ui.sunnypilot.ui_state import ui_state_sp
class SunnylinkLayoutMici(NavWidget):
def __init__(self, back_callback: Callable):
super().__init__()
self.set_back_callback(back_callback)
self._restore_in_progress = False
self._backup_in_progress = False
self._sunnylink_enabled = ui_state_sp.params.get("SunnylinkEnabled")
self._sunnylink_toggle = BigToggle(text="enable sunnylink",
initial_state=self._sunnylink_enabled,
toggle_callback=self._sunnylink_toggle_callback)
self._sunnylink_pair_button = SunnylinkPairBigButton()
self._sunnylink_sponsor_button = SunnylinkPairBigButton(sponsor_pairing=True)
self._backup_btn = BigButton("backup settings", "", "")
self._backup_btn.set_click_callback(lambda: self._handle_backup_restore_btn(restore=False))
self._restore_btn = BigButton("restore settings", "", "")
self._restore_btn.set_click_callback(lambda: self._handle_backup_restore_btn(restore=True))
self._scroller = Scroller([
self._sunnylink_toggle,
self._sunnylink_pair_button,
self._sunnylink_sponsor_button,
self._backup_btn,
self._restore_btn,
], snap_items=False)
def _update_state(self):
super()._update_state()
self._sunnylink_enabled = ui_state_sp.sunnylink_enabled
self._sunnylink_toggle.set_text("disable sunnylink" if self._sunnylink_enabled else "enable sunnylink")
self._sunnylink_pair_button.set_visible(self._sunnylink_enabled)
self._sunnylink_sponsor_button.set_visible(self._sunnylink_enabled)
self._backup_btn.set_visible(self._sunnylink_enabled)
self._restore_btn.set_visible(self._sunnylink_enabled)
self.handle_backup_restore_progress()
def show_event(self):
super().show_event()
self._scroller.show_event()
ui_state_sp.update_params()
def _render(self, rect: rl.Rectangle):
self._scroller.render(rect)
def _sunnylink_toggle_callback(self, state: bool):
ui_state_sp.params.put_bool("SunnylinkEnabled", state)
ui_state_sp.update_params()
def _handle_backup_restore_btn(self, restore:bool = False):
lbl = tr("slide to restore") if restore else tr("slide to backup")
icon = "icons_mici/settings/network/new/trash.png"
dlg = BigConfirmationDialogV2(lbl, icon, confirm_callback=None)
gui_app.set_modal_overlay(dlg, callback=self._restore_handler if restore else self._backup_handler)
def _backup_handler(self, dialog_result: int):
self._backup_in_progress = True
self._backup_btn.set_enabled(False)
ui_state_sp.params.put_bool("BackupManager_CreateBackup", True)
def _restore_handler(self, dialog_result: int):
self._restore_in_progress = True
self._restore_btn.set_enabled(False)
ui_state_sp.params.put("BackupManager_RestoreVersion", "latest")
def handle_backup_restore_progress(self):
sunnylink_backup_manager = ui_state_sp.sm["backupManagerSP"]
backup_status = sunnylink_backup_manager.backupStatus
restore_status = sunnylink_backup_manager.restoreStatus
backup_progress = sunnylink_backup_manager.backupProgress
restore_progress = sunnylink_backup_manager.restoreProgress
if self._backup_in_progress:
if backup_status == custom.BackupManagerSP.Status.inProgress:
text = tr(f"{backup_progress}%")
self._backup_btn.set_value(text)
elif backup_status == custom.BackupManagerSP.Status.failed:
self._backup_in_progress = False
self._backup_btn.set_enabled(not ui_state_sp.is_onroad())
self._backup_btn.set_value(tr("Failed"))
elif backup_status == custom.BackupManagerSP.Status.completed:
self._backup_in_progress = False
dialog = BigDialog(title=tr("Settings backed up"), description="")
gui_app.set_modal_overlay(dialog)
self._backup_btn.set_enabled(not ui_state_sp.is_onroad())
elif self._restore_in_progress:
if restore_status == custom.BackupManagerSP.Status.inProgress:
self._restore_btn.set_enabled(False)
text = tr(f"{restore_progress}%")
self._restore_btn.set_value(text)
elif restore_status == custom.BackupManagerSP.Status.failed:
self._restore_in_progress = False
self._restore_btn.set_enabled(not ui_state_sp.is_onroad())
self._restore_btn.set_value(tr("Failed"))
dialog = BigDialog(title=tr("Unable to restore"), description="Try again later.")
gui_app.set_modal_overlay(dialog)
elif restore_status == custom.BackupManagerSP.Status.completed:
self._restore_in_progress = False
dialog = BigConfirmationDialogV2("slide to restart", "", confirm_callback=None)
gui_app.set_modal_overlay(dialog, callback=lambda: {
gui_app.request_close()
})
else:
can_enable = self._sunnylink_enabled and not ui_state_sp.is_onroad()
self._backup_btn.set_enabled(can_enable)
self._backup_btn.set_text(tr("backup settings"))
self._restore_btn.set_enabled(can_enable)
self._restore_btn.set_text(tr("restore settings"))
class SunnylinkPairBigButton(BigButton):
def __init__(self, sponsor_pairing: bool = False):
self.sponsor_pairing = sponsor_pairing
super().__init__("", "", "")
def _update_state(self):
self.set_value("")
self.set_enabled(True)
if self.sponsor_pairing:
if ui_state_sp.sunnylink_state.is_sponsor():
self.set_text("thanks")
self.set_value("for sponsoring")
self.set_enabled(False)
else:
self.set_text("sponsor")
else:
if ui_state_sp.sunnylink_state.is_paired():
self.set_text(tr("paired"))
self.set_enabled(False)
else:
self.set_text(tr("pair"))
def _handle_mouse_release(self, mouse_pos: MousePos):
super()._handle_mouse_release(mouse_pos)
dlg: BigDialog | SunnylinkPairingDialog | None = None
if UNREGISTERED_SUNNYLINK_DONGLE_ID == (ui_state_sp.params.get("SunnylinkDongleId") or UNREGISTERED_SUNNYLINK_DONGLE_ID):
dlg = BigDialog(tr("sunnylink Dongle ID not found. Please reboot & try again."), "")
elif self.sponsor_pairing and not ui_state_sp.sunnylink_state.is_sponsor():
dlg = SunnylinkPairingDialog(sponsor_pairing=True)
elif not self.sponsor_pairing and not ui_state_sp.sunnylink_state.is_paired():
dlg = SunnylinkPairingDialog(sponsor_pairing=False)
if dlg:
gui_app.set_modal_overlay(dlg)

View File

@@ -0,0 +1,53 @@
import base64
import pyray as rl
from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.ui.sunnypilot.ui_state import ui_state_sp
from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog
from openpilot.sunnypilot.sunnylink.api import SunnylinkApi, UNREGISTERED_SUNNYLINK_DONGLE_ID, API_HOST
from openpilot.system.ui.lib.application import FontWeight, gui_app
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets.label import MiciLabel
class SunnylinkPairingDialog(PairingDialog):
"""Dialog for device pairing with QR code."""
QR_REFRESH_INTERVAL = 300 # 5 minutes in seconds
def __init__(self, sponsor_pairing: bool = False):
PairingDialog.__init__(self)
self._sponsor_pairing = sponsor_pairing
label_text = tr("become a sunnypilot sponsor") if sponsor_pairing else tr("pair with sunnylink")
self._pair_label = MiciLabel(label_text, 48, font_weight=FontWeight.BOLD,
color=rl.Color(255, 255, 255, int(255 * 0.9)), line_height=40, wrap_text=True)
def _get_pairing_url(self) -> str:
qr_string = "https://github.com/sponsors/sunnyhaibin"
if self._sponsor_pairing:
try:
sl_dongle_id = ui_state_sp.params.get("SunnylinkDongleId") or UNREGISTERED_SUNNYLINK_DONGLE_ID
token = SunnylinkApi(sl_dongle_id).get_token()
inner_string = f"1|{sl_dongle_id}|{token}"
payload_bytes = base64.b64encode(inner_string.encode('utf-8')).decode('utf-8')
qr_string = f"{API_HOST}/sso?state={payload_bytes}"
except Exception:
cloudlog.exception("Failed to get pairing token")
return qr_string
def _update_state(self):
if ui_state_sp.sunnylink_state.is_paired():
self._playing_dismiss_animation = True
if __name__ == "__main__":
gui_app.init_window("pairing device")
pairing = SunnylinkPairingDialog(sponsor_pairing=True)
try:
for _ in gui_app.render():
result = pairing.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
if result != -1:
break
finally:
del pairing

View File

@@ -1,3 +1,5 @@
from collections.abc import Callable
from openpilot.selfdrive.ui.ui_state import UIState
from cereal import messaging
from openpilot.common.params import Params
@@ -19,15 +21,24 @@ class UIStateSP(UIState):
]
self.sm = messaging.SubMaster(op_services + sp_services)
# Callbacks
self._ui_update_callbacks: list[Callable[[], None]] = []
def add_ui_update_callback(self, callback: Callable[[], None]):
self._ui_update_callbacks.append(callback)
def update(self) -> None:
UIState.update(self)
self.sunnylink_state.start()
for callback in self._ui_update_callbacks:
callback()
def _update_status(self) -> None:
UIState._update_status(self)
def update_params(self) -> None:
UIState.update_params(self)
self.sunnylink_enabled = self.params.get_bool("SunnylinkEnabled")
# Global instance
ui_state_sp = UIStateSP()

View File

@@ -41,47 +41,47 @@ def put_update_params(params: Params):
params.put("UpdaterTargetBranch", BRANCH_NAME)
def setup_homescreen(click, pm: PubMaster):
def setup_homescreen(click, pm: PubMaster, scroll=None):
pass
def setup_homescreen_update_available(click, pm: PubMaster):
def setup_homescreen_update_available(click, pm: PubMaster, scroll=None):
params = Params()
params.put_bool("UpdateAvailable", True)
put_update_params(params)
setup_offroad_alert(click, pm)
def setup_settings(click, pm: PubMaster):
def setup_settings(click, pm: PubMaster, scroll=None):
click(100, 100)
def close_settings(click, pm: PubMaster):
def close_settings(click, pm: PubMaster, scroll=None):
click(240, 216)
def setup_settings_network(click, pm: PubMaster):
def setup_settings_network(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
click(278, 450)
def setup_settings_network_advanced(click, pm: PubMaster):
setup_settings_network(click, pm)
def setup_settings_network_advanced(click, pm: PubMaster, scroll=None):
setup_settings_network(click, pm, scroll=scroll)
click(1880, 100)
def setup_settings_toggles(click, pm: PubMaster):
setup_settings(click, pm)
click(278, 600)
def setup_settings_software(click, pm: PubMaster):
put_update_params(Params())
def setup_settings_toggles(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
click(278, 720)
def setup_settings_software_download(click, pm: PubMaster):
def setup_settings_software(click, pm: PubMaster, scroll=None):
put_update_params(Params())
setup_settings(click, pm)
click(278, 845)
def setup_settings_software_download(click, pm: PubMaster, scroll=None):
params = Params()
# setup_settings_software but with "DOWNLOAD" button to test long text
params.put("UpdaterState", "idle")
@@ -89,13 +89,13 @@ def setup_settings_software_download(click, pm: PubMaster):
setup_settings_software(click, pm)
def setup_settings_software_release_notes(click, pm: PubMaster):
setup_settings_software(click, pm)
def setup_settings_software_release_notes(click, pm: PubMaster, scroll=None):
setup_settings_software(click, pm, scroll=scroll)
click(588, 110) # expand description for current version
def setup_settings_software_branch_switcher(click, pm: PubMaster):
setup_settings_software(click, pm)
def setup_settings_software_branch_switcher(click, pm: PubMaster, scroll=None):
setup_settings_software(click, pm, scroll=scroll)
params = Params()
params.put("UpdaterAvailableBranches", f"master,nightly,release,{BRANCH_NAME}")
params.put("GitBranch", BRANCH_NAME) # should be on top
@@ -103,30 +103,32 @@ def setup_settings_software_branch_switcher(click, pm: PubMaster):
click(1984, 449)
def setup_settings_firehose(click, pm: PubMaster):
def setup_settings_firehose(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
click(278, 845)
scroll(-1000, 278, 950)
click(278, 950)
def setup_settings_developer(click, pm: PubMaster):
def setup_settings_developer(click, pm: PubMaster, scroll=None):
CP = car.CarParams()
CP.alphaLongitudinalAvailable = True # show alpha long control toggle
Params().put("CarParamsPersistent", CP.to_bytes())
setup_settings(click, pm)
click(278, 950)
scroll(-1000, 278, 950)
click(278, 1040)
def setup_keyboard(click, pm: PubMaster):
setup_settings_developer(click, pm)
def setup_keyboard(click, pm: PubMaster, scroll=None):
setup_settings_developer(click, pm, scroll=scroll)
click(1930, 470)
def setup_pair_device(click, pm: PubMaster):
def setup_pair_device(click, pm: PubMaster, scroll=None):
click(1950, 800)
def setup_offroad_alert(click, pm: PubMaster):
def setup_offroad_alert(click, pm: PubMaster, scroll=None):
put_update_params(Params())
set_offroad_alert("Offroad_TemperatureTooHigh", True, extra_text='99C')
set_offroad_alert("Offroad_ExcessiveActuation", True, extra_text='longitudinal')
@@ -137,22 +139,63 @@ def setup_offroad_alert(click, pm: PubMaster):
close_settings(click, pm)
def setup_confirmation_dialog(click, pm: PubMaster):
def setup_confirmation_dialog(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
click(1985, 791) # reset calibration
def setup_experimental_mode_description(click, pm: PubMaster):
def setup_experimental_mode_description(click, pm: PubMaster, scroll=None):
setup_settings_toggles(click, pm)
click(1200, 280) # expand description for experimental mode
def setup_openpilot_long_confirmation_dialog(click, pm: PubMaster):
setup_settings_developer(click, pm)
def setup_openpilot_long_confirmation_dialog(click, pm: PubMaster, scroll=None):
setup_settings_developer(click, pm, scroll=scroll)
click(650, 960) # toggle openpilot longitudinal control
def setup_settings_sunnylink(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
click(278, 600)
def setup_onroad(click, pm: PubMaster):
def setup_settings_models(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
click(278, 950)
def setup_settings_steering(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
click(278, 1040)
def setup_settings_cruise(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
scroll(-40, 278, 950)
click(278, 1040)
def setup_settings_visuals(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
scroll(-1000, 278, 950)
click(278, 330)
def setup_settings_display(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
scroll(-1000, 278, 950)
click(278, 450)
def setup_settings_osm(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
scroll(-1000, 278, 950)
click(278, 600)
def setup_settings_trips(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
scroll(-1000, 278, 950)
click(278, 720)
def setup_settings_vehicle(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
scroll(-1000, 278, 950)
click(278, 845)
def setup_onroad(click, pm: PubMaster, scroll=None):
ds = messaging.new_message('deviceState')
ds.deviceState.started = True
@@ -173,7 +216,7 @@ def setup_onroad(click, pm: PubMaster):
time.sleep(0.05)
def setup_onroad_sidebar(click, pm: PubMaster):
def setup_onroad_sidebar(click, pm: PubMaster, scroll=None):
setup_onroad(click, pm)
click(100, 100) # open sidebar
@@ -192,23 +235,23 @@ def setup_onroad_alert(click, pm: PubMaster, size: log.SelfdriveState.AlertSize,
time.sleep(0.05)
def setup_onroad_small_alert(click, pm: PubMaster):
def setup_onroad_small_alert(click, pm: PubMaster, scroll=None):
setup_onroad_alert(click, pm, AlertSize.small, "Small Alert", "This is a small alert", AlertStatus.normal)
def setup_onroad_medium_alert(click, pm: PubMaster):
def setup_onroad_medium_alert(click, pm: PubMaster, scroll=None):
setup_onroad_alert(click, pm, AlertSize.mid, "Medium Alert", "This is a medium alert", AlertStatus.userPrompt)
def setup_onroad_full_alert(click, pm: PubMaster):
def setup_onroad_full_alert(click, pm: PubMaster, scroll=None):
setup_onroad_alert(click, pm, AlertSize.full, "DISENGAGE IMMEDIATELY", "Driver Distracted", AlertStatus.critical)
def setup_onroad_full_alert_multiline(click, pm: PubMaster):
def setup_onroad_full_alert_multiline(click, pm: PubMaster, scroll=None):
setup_onroad_alert(click, pm, AlertSize.full, "Reverse\nGear", "", AlertStatus.normal)
def setup_onroad_full_alert_long_text(click, pm: PubMaster):
def setup_onroad_full_alert_long_text(click, pm: PubMaster, scroll=None):
setup_onroad_alert(click, pm, AlertSize.full, "TAKE CONTROL IMMEDIATELY", "Calibration Invalid: Remount Device & Recalibrate", AlertStatus.userPrompt)
@@ -243,6 +286,19 @@ CASES = {
"onroad_full_alert_long_text": setup_onroad_full_alert_long_text,
}
# sunnypilot cases
CASES.update({
"settings_sunnylink": setup_settings_sunnylink,
"settings_models": setup_settings_models,
"settings_steering": setup_settings_steering,
"settings_cruise": setup_settings_cruise,
"settings_visuals": setup_settings_visuals,
"settings_display": setup_settings_display,
"settings_osm": setup_settings_osm,
"settings_trips": setup_settings_trips,
"settings_vehicle": setup_settings_vehicle,
})
class TestUI:
def __init__(self):
@@ -276,11 +332,15 @@ class TestUI:
time.sleep(0.01)
pyautogui.mouseUp(self.ui.left + x, self.ui.top + y, *args, **kwargs)
def scroll(self, clicks, x, y, *args, **kwargs):
pyautogui.scroll(clicks, self.ui.left + x, self.ui.top + y, *args, **kwargs)
time.sleep(UI_DELAY)
@with_processes(["ui"])
def test_ui(self, name, setup_case):
self.setup()
time.sleep(UI_DELAY) # wait for UI to start
setup_case(self.click, self.pm)
setup_case(self.click, self.pm, self.scroll)
self.screenshot(name)

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,130 @@
import base64
import pyray as rl
from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.ui.sunnypilot.ui_state import ui_state_sp
from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog
from openpilot.sunnypilot.sunnylink.api import SunnylinkApi, UNREGISTERED_SUNNYLINK_DONGLE_ID, API_HOST
from openpilot.system.ui.lib.application import FontWeight, gui_app
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.wrap_text import wrap_text
from openpilot.system.ui.lib.text_measure import measure_text_cached
class SunnylinkPairingDialog(PairingDialog):
"""Dialog for device pairing with QR code."""
QR_REFRESH_INTERVAL = 300 # 5 minutes in seconds
def __init__(self, sponsor_pairing: bool = False):
PairingDialog.__init__(self)
self._sponsor_pairing = sponsor_pairing
def _get_pairing_url(self) -> str:
qr_string = "https://github.com/sponsors/sunnyhaibin"
if self._sponsor_pairing:
try:
sl_dongle_id = self.params.get("SunnylinkDongleId") or UNREGISTERED_SUNNYLINK_DONGLE_ID
token = SunnylinkApi(sl_dongle_id).get_token()
inner_string = f"1|{sl_dongle_id}|{token}"
payload_bytes = base64.b64encode(inner_string.encode('utf-8')).decode('utf-8')
qr_string = f"{API_HOST}/sso?state={payload_bytes}"
except Exception:
cloudlog.exception("Failed to get pairing token")
return qr_string
def _update_state(self):
if ui_state_sp.sunnylink_state.is_paired():
gui_app.set_modal_overlay(None)
def _render(self, rect: rl.Rectangle) -> int:
rl.clear_background(rl.Color(224, 224, 224, 255))
self._check_qr_refresh()
margin = 70
content_rect = rl.Rectangle(rect.x + margin, rect.y + margin, rect.width - 2 * margin, rect.height - 2 * margin)
y = content_rect.y
# Close button
close_size = 80
pad = 20
close_rect = rl.Rectangle(content_rect.x - pad, y - pad, close_size + pad * 2, close_size + pad * 2)
self._close_btn.render(close_rect)
y += close_size + 40
# Title
title = tr("Pair your GitHub account") if self._sponsor_pairing else tr("Early Access: Become a sunnypilot Sponsor")
title_font = gui_app.font(FontWeight.NORMAL)
left_width = int(content_rect.width * 0.5 - 15)
title_wrapped = wrap_text(title_font, title, 75, left_width)
rl.draw_text_ex(title_font, "\n".join(title_wrapped), rl.Vector2(content_rect.x, y), 75, 0.0, rl.BLACK)
y += len(title_wrapped) * 75 + 60
# Two columns: instructions and QR code
remaining_height = content_rect.height - (y - content_rect.y)
right_width = content_rect.width // 2 - 20
# Instructions
self._render_instructions(rl.Rectangle(content_rect.x, y, left_width, remaining_height))
# QR code
qr_size = min(right_width, content_rect.height) - 40
qr_x = content_rect.x + left_width + 40 + (right_width - qr_size) // 2
qr_y = content_rect.y
self._render_qr_code(rl.Rectangle(qr_x, qr_y, qr_size, qr_size))
return -1
def _render_instructions(self, rect: rl.Rectangle) -> None:
if self._sponsor_pairing:
instructions = [
tr("Scan the QR code to login to your GitHub account"),
tr("Follow the prompts to complete the pairing process"),
tr("Re-enter the \"sunnylink\" panel to verify sponsorship status"),
tr("If sponsorship status was not updated, please contact a moderator on our forum at https://community.sunnypilot.ai")
]
else:
instructions = [
tr("Scan the QR code to visit sunnyhaibin's GitHub Sponsors page"),
tr("Choose your sponsorship tier and confirm your support"),
tr("Join our Community Forum at https://community.sunnypilot.ai and reach out to a moderator if you have issues")
]
font = gui_app.font(FontWeight.BOLD)
y = rect.y
for i, text in enumerate(instructions):
circle_radius = 25
circle_x = rect.x + circle_radius + 15
text_x = rect.x + circle_radius * 2 + 40
text_width = rect.width - (circle_radius * 2 + 40)
wrapped = wrap_text(font, text, 47, int(text_width))
text_height = len(wrapped) * 47
circle_y = y + text_height // 2
# Circle and number
rl.draw_circle(int(circle_x), int(circle_y), circle_radius, rl.Color(70, 70, 70, 255))
number = str(i + 1)
number_size = measure_text_cached(font, number, 30)
rl.draw_text_ex(font, number, (int(circle_x - number_size.x // 2), int(circle_y - number_size.y // 2)), 30, 0, rl.WHITE)
# Text
rl.draw_text_ex(font, "\n".join(wrapped), rl.Vector2(text_x, y), 47, 0.0, rl.BLACK)
y += text_height + 50
if __name__ == "__main__":
gui_app.init_window("pairing device")
pairing = SunnylinkPairingDialog(sponsor_pairing=True)
try:
for _ in gui_app.render():
result = pairing.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
if result != -1:
break
finally:
del pairing