mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 00:53:45 +08:00
ui: qr widget (#38951)
* rewrite pairing dialog into nice qr widget * simplify, move refresh to device pairing dialog * fix * refactor
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:25db08a9038b5faa3b200ef5625cb216d7f4b31ea3d49fb0395f9b726bfaf8e6
|
||||
size 13135
|
||||
@@ -3,7 +3,7 @@ import numpy as np
|
||||
import pyray as rl
|
||||
from collections.abc import Callable
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.qrcode import make_texture
|
||||
from openpilot.selfdrive.ui.mici.widgets.qr import QR
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app, TextAlignment
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.button import SmallCircleIconButton
|
||||
@@ -275,27 +275,6 @@ class TrainingGuide(NavWidget):
|
||||
self._steps[0].render(self._rect)
|
||||
|
||||
|
||||
class QRCodeWidget(Widget):
|
||||
def __init__(self, url: str, size: int = 170):
|
||||
super().__init__()
|
||||
self.set_rect(rl.Rectangle(0, 0, size, size))
|
||||
self._size = size
|
||||
self._qr_texture: rl.Texture | None = None
|
||||
self._generate_qr(url)
|
||||
|
||||
def _generate_qr(self, url: str):
|
||||
self._qr_texture = make_texture(url, inverted=True)
|
||||
|
||||
def _render(self, _):
|
||||
if self._qr_texture:
|
||||
scale = self._size / self._qr_texture.height
|
||||
rl.draw_texture_ex(self._qr_texture, rl.Vector2(round(self._rect.x), round(self._rect.y)), 0.0, scale, rl.WHITE)
|
||||
|
||||
def __del__(self):
|
||||
if self._qr_texture and self._qr_texture.id != 0:
|
||||
rl.unload_texture(self._qr_texture)
|
||||
|
||||
|
||||
class TermsPage(Scroller):
|
||||
def __init__(self, on_accept, on_decline):
|
||||
super().__init__()
|
||||
@@ -312,7 +291,7 @@ class TermsPage(Scroller):
|
||||
self._terms_header,
|
||||
GreyBigButton("swipe for QR code", "or go to https://comma.ai/terms",
|
||||
gui_app.texture("icons_mici/setup/small_slider/slider_arrow.png", 64, 56, flip_x=True)),
|
||||
QRCodeWidget("https://comma.ai/terms"),
|
||||
QR("https://comma.ai/terms"),
|
||||
self._must_accept_card,
|
||||
self._accept_button,
|
||||
self._decline_button,
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import os
|
||||
import pyray as rl
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from typing import Union
|
||||
|
||||
from openpilot.common.api import Api
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.time_helpers import system_time_valid
|
||||
from openpilot.system.ui.widgets.scroller import NavRawScrollPanel, NavScroller
|
||||
from openpilot.selfdrive.ui.mici.widgets.info import InfoLayoutMici
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigCircleButton
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigCircleButton, GreyBigButton
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog
|
||||
from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog
|
||||
from openpilot.selfdrive.ui.mici.widgets.qr import QR
|
||||
from openpilot.selfdrive.ui.mici.onroad.cabin_camera_dialog import CabinCameraDialog
|
||||
from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide, TermsPage
|
||||
from openpilot.system.ui.lib.application import gui_app, MousePos
|
||||
@@ -96,6 +99,53 @@ class DeviceInfoLayoutMici(InfoLayoutMici):
|
||||
super().__init__("device ID", params.get("DongleId") or 'N/A', "serial", params.get("HardwareSerial") or 'N/A', width=380)
|
||||
|
||||
|
||||
class PairingDialog(NavScroller):
|
||||
"""Pair the device with comma connect."""
|
||||
|
||||
QR_REFRESH_INTERVAL = 300 # 5 minutes in seconds
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
self._last_pairing_qr_generation = float("-inf")
|
||||
self._qr = QR(self._get_pairing_url())
|
||||
self._scroller._show_scroll_indicator = False
|
||||
self._scroller.add_widgets([
|
||||
self._qr,
|
||||
GreyBigButton("finish setup", "scan to pair device\nwith connect",
|
||||
gui_app.texture("icons_mici/settings/device/green_settings.png", 64, 64)),
|
||||
GreyBigButton("", "connect lets you review recent driving footage and bookmark events."),
|
||||
])
|
||||
|
||||
def _get_pairing_url(self) -> str:
|
||||
try:
|
||||
dongle_id = self._params.get("DongleId") or ""
|
||||
token = Api(dongle_id).get_token({'pair': True})
|
||||
except Exception as e:
|
||||
cloudlog.warning(f"Failed to get pairing token: {e}")
|
||||
token = ""
|
||||
return f"https://connect.comma.ai/?pair={token}"
|
||||
|
||||
def _update_layout_rects(self):
|
||||
super()._update_layout_rects()
|
||||
self._qr.set_rect(rl.Rectangle(self._qr.rect.x, self._qr.rect.y, self._rect.height, self._rect.height))
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
current_time = time.monotonic()
|
||||
if current_time - self._last_pairing_qr_generation >= self.QR_REFRESH_INTERVAL:
|
||||
self._qr._url = self._get_pairing_url()
|
||||
if self._qr._texture and self._qr._texture.id != 0:
|
||||
rl.unload_texture(self._qr._texture)
|
||||
self._qr._texture = self._qr._generate_qr_code()
|
||||
self._last_pairing_qr_generation = current_time
|
||||
super()._render(rect)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
if ui_state.prime_state.is_paired() and not self.is_dismissing:
|
||||
self.dismiss()
|
||||
|
||||
|
||||
class PairBigButton(BigButton):
|
||||
def __init__(self):
|
||||
super().__init__("pair", "connect.comma.ai", gui_app.texture("icons_mici/settings/comma_icon.png", 33, 60))
|
||||
|
||||
@@ -52,7 +52,7 @@ class TestWidgetLeaks(OpenpilotTestCase):
|
||||
# mici dialogs
|
||||
from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide as MiciTrainingGuide, OnboardingWindow as MiciOnboardingWindow
|
||||
from openpilot.selfdrive.ui.mici.onroad.cabin_camera_dialog import CabinCameraDialog as MiciCabinCameraDialog
|
||||
from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog as MiciPairingDialog
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.device import PairingDialog as MiciPairingDialog
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog, BigInputDialog
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.device import MiciFccModal
|
||||
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
import pyray as rl
|
||||
import time
|
||||
|
||||
from openpilot.common.api import Api
|
||||
from openpilot.common.qrcode import make_texture
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.widgets.nav_widget import NavWidget
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
|
||||
|
||||
class PairingDialog(NavWidget):
|
||||
"""Dialog for device pairing with QR code."""
|
||||
|
||||
QR_REFRESH_INTERVAL = 300 # 5 minutes in seconds
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
self._qr_texture: rl.Texture | None = None
|
||||
self._last_qr_generation = float("-inf")
|
||||
|
||||
self._txt_pair = gui_app.texture("icons_mici/settings/device/pair.png", 33, 60)
|
||||
self._pair_label = UnifiedLabel("pair with comma connect", font_size=48, font_weight=FontWeight.BOLD, line_height=0.8)
|
||||
|
||||
def _get_pairing_url(self) -> str:
|
||||
try:
|
||||
dongle_id = self._params.get("DongleId") or ""
|
||||
token = Api(dongle_id).get_token({'pair': True})
|
||||
except Exception as e:
|
||||
cloudlog.warning(f"Failed to get pairing token: {e}")
|
||||
token = ""
|
||||
return f"https://connect.comma.ai/?pair={token}"
|
||||
|
||||
def _generate_qr_code(self) -> None:
|
||||
try:
|
||||
if self._qr_texture and self._qr_texture.id != 0:
|
||||
rl.unload_texture(self._qr_texture)
|
||||
self._qr_texture = make_texture(self._get_pairing_url(), inverted=True)
|
||||
except Exception as e:
|
||||
cloudlog.warning(f"QR code generation failed: {e}")
|
||||
self._qr_texture = None
|
||||
|
||||
def _check_qr_refresh(self) -> None:
|
||||
current_time = time.monotonic()
|
||||
if current_time - self._last_qr_generation >= self.QR_REFRESH_INTERVAL:
|
||||
self._generate_qr_code()
|
||||
self._last_qr_generation = current_time
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
if ui_state.prime_state.is_paired() and not self.is_dismissing:
|
||||
self.dismiss()
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
self._check_qr_refresh()
|
||||
|
||||
self._render_qr_code()
|
||||
|
||||
label_x = self._rect.x + 8 + self._rect.height + 24
|
||||
self._pair_label.set_max_width(int(self._rect.width - label_x))
|
||||
self._pair_label.set_position(label_x, self._rect.y + 16)
|
||||
self._pair_label.render()
|
||||
|
||||
rl.draw_texture_ex(self._txt_pair, rl.Vector2(label_x, self._rect.y + self._rect.height - self._txt_pair.height - 16),
|
||||
0.0, 1.0, rl.Color(255, 255, 255, int(255 * 0.35)))
|
||||
|
||||
def _render_qr_code(self) -> None:
|
||||
if not self._qr_texture:
|
||||
error_font = gui_app.font(FontWeight.BOLD)
|
||||
rl.draw_text_ex(
|
||||
error_font, "QR Code Error", rl.Vector2(self._rect.x + 20, self._rect.y + self._rect.height // 2 - 15), 30, 0.0, rl.RED
|
||||
)
|
||||
return
|
||||
|
||||
scale = self._rect.height / self._qr_texture.height
|
||||
pos = rl.Vector2(round(self._rect.x + 8), round(self._rect.y))
|
||||
rl.draw_texture_ex(self._qr_texture, pos, 0.0, scale, rl.WHITE)
|
||||
|
||||
def __del__(self):
|
||||
if self._qr_texture and self._qr_texture.id != 0:
|
||||
rl.unload_texture(self._qr_texture)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
gui_app.init_window("pairing device")
|
||||
pairing = PairingDialog()
|
||||
gui_app.push_widget(pairing)
|
||||
try:
|
||||
for _ in gui_app.render():
|
||||
pass
|
||||
finally:
|
||||
del pairing
|
||||
@@ -0,0 +1,37 @@
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.ui.lib.application import FontWeight
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.label import Label
|
||||
from openpilot.common.qrcode import make_texture
|
||||
|
||||
|
||||
class QR(Widget):
|
||||
def __init__(self, url: str, width: int = 170):
|
||||
super().__init__()
|
||||
self._url = url
|
||||
|
||||
self._texture = self._generate_qr_code()
|
||||
|
||||
self.set_rect(rl.Rectangle(0, 0, width, width))
|
||||
self._error = Label("QR Code Error", font_size=30, font_weight=FontWeight.BOLD, text_color=rl.RED)
|
||||
|
||||
def _generate_qr_code(self):
|
||||
try:
|
||||
return make_texture(self._url, inverted=True)
|
||||
except Exception as e:
|
||||
cloudlog.warning(f"QR code generation failed: {e}")
|
||||
return None
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
if not self._texture:
|
||||
self._error.render(rect)
|
||||
return
|
||||
scale = rect.height / self._texture.height
|
||||
pos = rl.Vector2(round(rect.x), round(rect.y))
|
||||
rl.draw_texture_ex(self._texture, pos, 0.0, scale, rl.WHITE)
|
||||
|
||||
def __del__(self):
|
||||
if self._texture and self._texture.id != 0:
|
||||
rl.unload_texture(self._texture)
|
||||
Reference in New Issue
Block a user