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:
stef
2026-09-18 00:53:39 -04:00
committed by GitHub
parent 3cd1b6e483
commit 7635dec618
6 changed files with 95 additions and 121 deletions
@@ -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
+37
View File
@@ -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)