""" 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. """ 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.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 self._is_paired_prev = ui_state.sunnylink_state.is_paired() 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): is_paired = ui_state.sunnylink_state.is_paired() if not self._is_paired_prev and is_paired: gui_app.pop_widget() 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 the community 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