Files
IQ.Pilot/iqpilot/selfdrive/ui/mici/widgets/stock_pairing_dialog.py
2026-09-03 18:23:24 -05:00

196 lines
6.7 KiB
Python

"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
"""
import os
import pyray as rl
import qrcode
import numpy as np
import time
import jwt
from datetime import datetime, timedelta, UTC
from iqpilot.common.api.base import BaseApi
from iqpilot.common.swaglog import cloudlog
from iqpilot.common.params import Params
from iqpilot.konn3kt.registration import get_or_create_dongle_id, ensure_dev_pairing_identity
from iqpilot.system.hardware import HARDWARE, PC
from iqpilot.selfdrive.ui.ui_state import ui_state
from iqpilot.system.ui.widgets.nav_widget import NavWidget
from iqpilot.system.ui.lib.application import FontWeight, gui_app
from iqpilot.system.ui.widgets.label import UnifiedLabel
from iqpilot.system.ui.lib.multilang import tr
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(tr("pair with Konn3kt"), font_size=48, font_weight=FontWeight.BOLD, line_height=0.8)
def _get_pairing_url(self) -> str:
dev_pairing = PC and os.getenv("KONN3KT_DEV_PAIRING") == "1"
if dev_pairing:
try:
ensure_dev_pairing_identity(self._params, force_reset=os.getenv("KONN3KT_DEV_PAIRING_RESET") == "1")
except Exception:
return "error://dev_identity_setup_failed"
try:
imei1 = HARDWARE.get_imei(0) or ""
except Exception as e:
cloudlog.warning(f"Failed to get imei1: {e}")
imei1 = ""
try:
imei2 = HARDWARE.get_imei(1) or ""
except Exception as e:
cloudlog.warning(f"Failed to get imei2: {e}")
imei2 = ""
try:
algorithm, private_key, public_key = BaseApi.get_key_pair()
if not private_key or not algorithm:
cloudlog.error("No device keys found")
return "error://keys_not_found"
dongle_id = get_or_create_dongle_id(self._params, prefer_readonly=True)
try:
serial = HARDWARE.get_serial() or ""
except Exception as e:
cloudlog.warning(f"Failed to get serial: {e}")
serial = ""
if not serial:
serial = (self._params.get("HardwareSerial") or "") if dev_pairing else ""
if not serial:
cloudlog.error("No hardware serial found, cannot generate pairing token")
return "error://serial_not_found"
now = datetime.now(UTC).replace(tzinfo=None)
payload = {
'identity': dongle_id,
'nbf': now,
'iat': now,
'imei': imei1,
'imei2': imei2,
'serial': serial,
'public_key': public_key,
'register': True,
'exp': now + timedelta(hours=1),
}
try:
token = jwt.encode(payload, private_key, algorithm=algorithm)
except Exception as e:
cloudlog.warning(f"jwt.encode failed ({e}), retrying with normalized key")
try:
from cryptography.hazmat.primitives import serialization
key_bytes = private_key.encode("utf-8") if isinstance(private_key, str) else private_key
try:
key_obj = serialization.load_pem_private_key(key_bytes, password=None)
except Exception:
key_obj = serialization.load_ssh_private_key(key_bytes, password=None)
token = jwt.encode(payload, key_obj, algorithm=algorithm)
except Exception as e2:
cloudlog.error(f"Failed to generate pairing token: {e2}")
return "error://token_generation_failed"
if isinstance(token, bytes):
token = token.decode('utf8')
return f"https://konn3kt.com/?pair={token}"
except FileNotFoundError as e:
cloudlog.error(f"Key files not found: {e}")
return "error://keys_not_found"
except Exception as e:
cloudlog.error(f"Failed to generate pairing token: {e}")
return "error://token_generation_failed"
def _generate_qr_code(self) -> None:
try:
url = self._get_pairing_url()
if url.startswith("error://"):
cloudlog.warning(f"Cannot generate QR code: {url}")
self._qr_texture = None
return
qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=10, border=0)
qr.add_data(url)
qr.make(fit=True)
pil_img = qr.make_image(fill_color="white", back_color="black").convert('RGBA')
img_array = np.array(pil_img, dtype=np.uint8)
if self._qr_texture and self._qr_texture.id != 0:
rl.unload_texture(self._qr_texture)
rl_image = rl.Image()
rl_image.data = rl.ffi.cast("void *", img_array.ctypes.data)
rl_image.width = pil_img.width
rl_image.height = pil_img.height
rl_image.mipmaps = 1
rl_image.format = rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8
self._qr_texture = rl.load_texture_from_image(rl_image)
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