diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py index b52f9ed39a..a83ebd1969 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/selfdrive/ui/mici/layouts/main.py @@ -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 diff --git a/selfdrive/ui/sunnypilot/mici/layouts/settings.py b/selfdrive/ui/sunnypilot/mici/layouts/settings.py new file mode 100644 index 0000000000..93ae3641ea --- /dev/null +++ b/selfdrive/ui/sunnypilot/mici/layouts/settings.py @@ -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) diff --git a/selfdrive/ui/sunnypilot/mici/layouts/sunnylink.py b/selfdrive/ui/sunnypilot/mici/layouts/sunnylink.py new file mode 100644 index 0000000000..96d826d781 --- /dev/null +++ b/selfdrive/ui/sunnypilot/mici/layouts/sunnylink.py @@ -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) diff --git a/selfdrive/ui/sunnypilot/mici/widgets/sunnylink_pairing_dialog.py b/selfdrive/ui/sunnypilot/mici/widgets/sunnylink_pairing_dialog.py new file mode 100644 index 0000000000..154b8612c1 --- /dev/null +++ b/selfdrive/ui/sunnypilot/mici/widgets/sunnylink_pairing_dialog.py @@ -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