init sunnylink panels

This commit is contained in:
nayan
2025-12-02 12:22:53 -05:00
parent eda189c564
commit 0f6ad56fb9
4 changed files with 265 additions and 0 deletions
+3
View File
@@ -11,6 +11,9 @@ from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.system.ui.lib.application import gui_app
if gui_app.sunnypilot_ui():
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.settings import SettingsLayoutSP as SettingsLayout
ONROAD_DELAY = 2.5 # seconds
@@ -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)
@@ -0,0 +1,169 @@
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.ui_state import ui_state
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.params.get("SunnylinkEnabled")
self._sunnylink_toggle = BigToggle(text="enable sunnylink",
initial_state=self._sunnylink_enabled,
toggle_callback=self._sunnylink_toggle_callback)
self._sunnylink_sponsor_button = SunnylinkPairBigButton(sponsor_pairing=False)
self._sunnylink_pair_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_sponsor_button,
self._sunnylink_pair_button,
self._backup_btn,
self._restore_btn,
], snap_items=False)
def _update_state(self):
super()._update_state()
self._sunnylink_enabled = ui_state.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()
if ui_state.sunnylink_state.is_sponsor():
self._sunnylink_sponsor_button.set_text("thanks")
self._sunnylink_sponsor_button.set_value("for sponsoring")
self._sunnylink_sponsor_button.set_enabled(False)
else:
self._sunnylink_sponsor_button.set_text("sponsor")
self._sunnylink_sponsor_button.set_value("")
if ui_state.sunnylink_state.is_paired():
self._sunnylink_pair_button.set_text("paired")
self._sunnylink_pair_button.set_enabled(False)
else:
self._sunnylink_pair_button.set_text("pair")
def show_event(self):
super().show_event()
self._scroller.show_event()
ui_state.update_params()
def _render(self, rect: rl.Rectangle):
self._scroller.render(rect)
def _sunnylink_toggle_callback(self, state: bool):
ui_state.params.put_bool("SunnylinkEnabled", state)
ui_state.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.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.params.put("BackupManager_RestoreVersion", "latest")
def handle_backup_restore_progress(self):
sunnylink_backup_manager = ui_state.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.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.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.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.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):
super()._update_state()
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.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.sunnylink_state.is_sponsor():
dlg = SunnylinkPairingDialog(sponsor_pairing=True)
elif not self.sponsor_pairing and not ui_state.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.ui_state import ui_state
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 import NavWidget
from openpilot.system.ui.widgets.label import MiciLabel
class SunnylinkPairingDialog(PairingDialog):
"""Dialog for device pairing with QR code."""
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.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):
NavWidget._update_state(self)
if self._sponsor_pairing and ui_state.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