mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-06-12 02:54:55 +08:00
Compare commits
40 Commits
only-chubb
...
mici-playg
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d57983c680 | ||
|
|
b3a9169a96 | ||
|
|
9e794892e3 | ||
|
|
abd19ca8da | ||
|
|
8fa22cd14c | ||
|
|
471621ff84 | ||
|
|
3f664201a6 | ||
|
|
75a3591cd6 | ||
|
|
5d3f95d420 | ||
|
|
f8d19fe9dd | ||
|
|
9d711350c2 | ||
|
|
45c853c87a | ||
|
|
e8ab9d812d | ||
|
|
e74252bdf5 | ||
|
|
b5a525b280 | ||
|
|
48d33e98e7 | ||
|
|
009d350a58 | ||
|
|
49e441dd1a | ||
|
|
2717d97350 | ||
|
|
64232397ed | ||
|
|
ffa78eabaa | ||
|
|
8c3cf8e542 | ||
|
|
bf673a38c8 | ||
|
|
4da32cc009 | ||
|
|
47687d4664 | ||
|
|
b7d1d82e1e | ||
|
|
63fe9d8f9c | ||
|
|
ddb71f81d7 | ||
|
|
b65c550cc8 | ||
|
|
6b45d2648b | ||
|
|
a8b319cfff | ||
|
|
6b1da28c01 | ||
|
|
d3d812dd92 | ||
|
|
b670c6a056 | ||
|
|
a9e57f0a76 | ||
|
|
712a358c94 | ||
|
|
632e9d13b2 | ||
|
|
51d0666c85 | ||
|
|
deda1329a2 | ||
|
|
4110749cb0 |
@@ -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
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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
|
||||
|
||||
24
selfdrive/ui/sunnypilot/layouts/settings/cruise.py
Normal file
24
selfdrive/ui/sunnypilot/layouts/settings/cruise.py
Normal 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()
|
||||
5
selfdrive/ui/sunnypilot/layouts/settings/device.py
Normal file
5
selfdrive/ui/sunnypilot/layouts/settings/device.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from openpilot.selfdrive.ui.layouts.settings.device import DeviceLayout
|
||||
|
||||
class DeviceLayoutSP(DeviceLayout):
|
||||
def __init__(self):
|
||||
DeviceLayout.__init__(self)
|
||||
24
selfdrive/ui/sunnypilot/layouts/settings/display.py
Normal file
24
selfdrive/ui/sunnypilot/layouts/settings/display.py
Normal 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()
|
||||
24
selfdrive/ui/sunnypilot/layouts/settings/models.py
Normal file
24
selfdrive/ui/sunnypilot/layouts/settings/models.py
Normal 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()
|
||||
24
selfdrive/ui/sunnypilot/layouts/settings/navigation.py
Normal file
24
selfdrive/ui/sunnypilot/layouts/settings/navigation.py
Normal 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()
|
||||
24
selfdrive/ui/sunnypilot/layouts/settings/osm.py
Normal file
24
selfdrive/ui/sunnypilot/layouts/settings/osm.py
Normal 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()
|
||||
189
selfdrive/ui/sunnypilot/layouts/settings/settings.py
Normal file
189
selfdrive/ui/sunnypilot/layouts/settings/settings.py
Normal 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
|
||||
24
selfdrive/ui/sunnypilot/layouts/settings/steering.py
Normal file
24
selfdrive/ui/sunnypilot/layouts/settings/steering.py
Normal 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()
|
||||
181
selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py
Normal file
181
selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py
Normal 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()
|
||||
24
selfdrive/ui/sunnypilot/layouts/settings/trips.py
Normal file
24
selfdrive/ui/sunnypilot/layouts/settings/trips.py
Normal 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()
|
||||
24
selfdrive/ui/sunnypilot/layouts/settings/vehicle.py
Normal file
24
selfdrive/ui/sunnypilot/layouts/settings/vehicle.py
Normal 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()
|
||||
24
selfdrive/ui/sunnypilot/layouts/settings/visuals.py
Normal file
24
selfdrive/ui/sunnypilot/layouts/settings/visuals.py
Normal 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()
|
||||
40
selfdrive/ui/sunnypilot/mici/layouts/settings.py
Normal file
40
selfdrive/ui/sunnypilot/mici/layouts/settings.py
Normal 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)
|
||||
170
selfdrive/ui/sunnypilot/mici/layouts/sunnylink.py
Normal file
170
selfdrive/ui/sunnypilot/mici/layouts/sunnylink.py
Normal 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)
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
BIN
sunnypilot/selfdrive/assets/offroad/icon_firehose.png
LFS
Normal file
BIN
sunnypilot/selfdrive/assets/offroad/icon_firehose.png
LFS
Normal file
Binary file not shown.
BIN
sunnypilot/selfdrive/assets/offroad/icon_home.png
LFS
Normal file
BIN
sunnypilot/selfdrive/assets/offroad/icon_home.png
LFS
Normal file
Binary file not shown.
130
system/ui/sunnypilot/widgets/sunnylink_pairing_dialog.py
Normal file
130
system/ui/sunnypilot/widgets/sunnylink_pairing_dialog.py
Normal 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
|
||||
Reference in New Issue
Block a user