IQ.Pilot Release Commit @ 0babf78

This commit is contained in:
IQ.Lvbs CI [bot]
2026-07-27 01:40:11 -05:00
parent 6fb5c0141c
commit b39791a93f
425 changed files with 12180 additions and 6137 deletions

View File

@@ -1,9 +1,9 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
"""
from openpilot.system.ui.iqpilot.widgets.list_view import IQButtonAction
from openpilot.system.ui.iqwidgets.widgets.list_view import IQButtonAction
class NoElideButtonAction(IQButtonAction):
class WideButtonAction(IQButtonAction):
def get_width_hint(self):
return super().get_width_hint() + 1

View File

@@ -7,9 +7,9 @@ import time
from collections.abc import Callable
import pyray as rl
from openpilot.system.ui.iqpilot.lib.styles import ink, metrics
from openpilot.system.ui.iqwidgets.lib.styles import ink, metrics
from openpilot.system.ui.widgets.button import Button, ButtonStyle
from openpilot.system.ui.iqpilot.lib import canvas
from openpilot.system.ui.iqwidgets.lib import canvas
from openpilot.system.ui.widgets.scroller_tici import LineSeparator, LINE_COLOR, LINE_PADDING
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.widgets.toggle import Toggle
@@ -899,8 +899,8 @@ def progress_item(title):
from dataclasses import dataclass, field
from openpilot.common.params import Params
from openpilot.system.ui.iqpilot.lib.styles import ink
from openpilot.system.ui.iqpilot.widgets.helpers.glyphs import draw_star
from openpilot.system.ui.iqwidgets.lib.styles import ink
from openpilot.system.ui.iqwidgets.widgets.helpers.glyphs import draw_star
from openpilot.system.ui.lib.application import FontWeight, gui_app
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr
@@ -925,13 +925,13 @@ _FRAME_PAD = 50
@dataclass
class TreeNode:
class PickerItem:
ref: str
data: dict = field(default_factory=dict)
@dataclass
class TreeFolder:
class PickerGroup:
folder: str
nodes: list
@@ -999,7 +999,7 @@ class _TreeRow(Button):
return super()._handle_mouse_release(mouse_pos)
class TreeOptionDialog(MultiOptionDialog):
class PickerDialog(MultiOptionDialog):
"""Folder/leaf picker with search, favourites, and a pinned current selection."""
def __init__(self, title, folders, current_ref="", fav_param="", option_font_weight=FontWeight.MEDIUM, search_prompt=None,
@@ -1018,7 +1018,7 @@ class TreeOptionDialog(MultiOptionDialog):
self.on_exit = on_exit
self.display_func = display_func or (lambda node: node.data.get('display_name', node.ref))
self.search_funcs = search_funcs or [lambda node: node.data.get('display_name', ''), lambda node: node.data.get('short_name', '')]
self.search_title = search_title or tr("Enter search query")
self.search_title = search_title or tr("Type to search")
self.search_subtitle = search_subtitle
self._search_rect: rl.Rectangle | None = None
self._search_pressed = False

View File

@@ -26,7 +26,7 @@ from openpilot.system.hardware import HARDWARE, PC
from openpilot.system.ui.lib.multilang import multilang
from openpilot.common.realtime import Ratekeeper
from openpilot.system.ui.iqpilot.lib.application import IQAppHooks
from openpilot.system.ui.iqwidgets.lib.application import IQAppHooks
from openpilot.system.ui.lib.screen_recorder import ScreenRecorder
_DEFAULT_FPS = int(os.getenv("FPS", {'tizi': 20, 'tici': 20}.get(HARDWARE.get_device_type(), 60)))

View File

@@ -33,10 +33,19 @@ EMOJI_REGEX = re.compile(
flags=re.UNICODE
)
_emoji_font_loaded = False
def _load_emoji_font() -> ImageFont.FreeTypeFont | None:
global _emoji_font
if _emoji_font is None:
_emoji_font = ImageFont.truetype(str(FONT_DIR.joinpath("NotoColorEmoji.ttf")), 109)
global _emoji_font, _emoji_font_loaded
if not _emoji_font_loaded:
_emoji_font_loaded = True
try:
# FONT_DIR is an importlib.resources path. Inside the setup zipapp it points into the archive,
# so str() yields a path through the .zip that PIL can't open ("cannot open resource"). Read
# the bytes and hand PIL a file object so it works both on disk and inside the zipapp.
_emoji_font = ImageFont.truetype(io.BytesIO(FONT_DIR.joinpath("NotoColorEmoji.ttf").read_bytes()), 109)
except Exception:
_emoji_font = None # never crash the whole UI over an emoji glyph
return _emoji_font
def find_emoji(text):
@@ -44,12 +53,15 @@ def find_emoji(text):
def emoji_tex(emoji):
if emoji not in _cache:
font = _load_emoji_font()
if font is None:
return None
img = Image.new("RGBA", (128, 128), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
draw.text((0, 0), emoji, font=_load_emoji_font(), embedded_color=True)
draw.text((0, 0), emoji, font=font, embedded_color=True)
with io.BytesIO() as buffer:
img.save(buffer, format="PNG")
l = buffer.tell()
buffer.seek(0)
_cache[emoji] = rl.load_texture_from_image(rl.load_image_from_memory(".png", buffer.getvalue(), l))
return _cache[emoji]
return _cache.get(emoji)

View File

@@ -67,6 +67,9 @@ class MeteredType(IntEnum):
NO = 2
_WARNED_UNSUPPORTED_NETWORKS: set[tuple[int, int, int]] = set()
def get_security_type(flags: int, wpa_flags: int, rsn_flags: int) -> SecurityType:
wpa_props = wpa_flags | rsn_flags
@@ -83,7 +86,10 @@ def get_security_type(flags: int, wpa_flags: int, rsn_flags: int) -> SecurityTyp
# WPA2, WPA2+WPA3 mixed, or WPA — all handled via WPA key_mgmt (NM negotiates SAE if available)
return SecurityType.WPA2
else:
cloudlog.warning(f"Unsupported network! flags: {flags}, wpa_flags: {wpa_flags}, rsn_flags: {rsn_flags}")
_key = (flags, wpa_flags, rsn_flags)
if _key not in _WARNED_UNSUPPORTED_NETWORKS:
_WARNED_UNSUPPORTED_NETWORKS.add(_key)
cloudlog.warning(f"Unsupported network! flags: {flags}, wpa_flags: {wpa_flags}, rsn_flags: {rsn_flags}")
return SecurityType.UNSUPPORTED
@@ -630,6 +636,7 @@ class WifiManager:
cloudlog.warning("No WiFi device found")
return
self._set_device_autoconnect(True)
self._connecting_to_ssid = ssid
self._router_main.send(new_method_call(self._nm, 'ActivateConnection', 'ooo',
(conn_path, self._wifi_device, "/")))
@@ -639,6 +646,37 @@ class WifiManager:
else:
threading.Thread(target=worker, daemon=True).start()
def disconnect_connection(self, ssid: str, block: bool = False):
def worker():
if self._router_main is None:
cloudlog.warning(f"WiFi not ready while disconnecting {ssid}")
return
if ssid not in self._get_connections():
return
# the profile stays saved and untouched; without clearing autoconnect on the device
# NetworkManager re-associates within seconds
self._set_device_autoconnect(False)
self._connecting_to_ssid = ""
self._deactivate_connection(ssid)
self._update_networks()
self._enqueue_callbacks(self._disconnected)
if block:
worker()
else:
threading.Thread(target=worker, daemon=True).start()
def _set_device_autoconnect(self, enabled: bool) -> None:
if self._router_main is None or self._wifi_device is None:
return
dev_addr = DBusAddress(self._wifi_device, bus_name=NM, interface=NM_DEVICE_IFACE)
reply = self._router_main.send_and_get_reply(Properties(dev_addr).set('Autoconnect', 'b', enabled))
if reply.header.message_type == MessageType.error:
cloudlog.warning(f'Failed to set device autoconnect={enabled}: {reply}')
def _deactivate_connection(self, ssid: str):
target_conn_path = self._get_connections().get(ssid, None)
if target_conn_path is None:

View File

@@ -50,6 +50,8 @@ INSTALLER_URL_PATH = "/tmp/installer_url"
# "<user>/<branch>" maps to a GitHub fork. IQ.OS uses the DRM "magic" compositor, not Wayland,
# so comma's downloaded installers (installer.comma.ai) crash on launch; clone the fork directly.
GITHUB_FORK_URL = "https://github.com/{user}/openpilot.git"
# IQ.Pilot lives on the IQ Lvbs git server; github.com/IQLvbs is DMCA'd and dead.
GIT_URL_OVERRIDES = {"IQLvbs": "https://git.konn3kt.com/IQ.Lvbs/IQ.Pilot.git"}
CONTINUE = """#!/usr/bin/env bash
@@ -756,7 +758,7 @@ class Setup(Widget):
pass
def _fork_install_thread(self, user: str, branch: str):
git_url = GITHUB_FORK_URL.format(user=user)
git_url = GIT_URL_OVERRIDES.get(user) or GITHUB_FORK_URL.format(user=user)
label = f"{user}/{branch}"
try:
subprocess.run(["rm", "-rf", TMP_INSTALL_PATH], check=False)
@@ -774,7 +776,8 @@ class Setup(Widget):
subprocess.run(["git", "-C", TMP_INSTALL_PATH, "submodule", "update", "--init"], check=False)
run_cmd(["rm", "-f", VALID_CACHE_PATH])
run_cmd(["rm", "-rf", INSTALL_PATH])
# sudo: a prior *run* install can leave root-owned .pyc here that a comma-user rm can't delete.
run_cmd(["sudo", "rm", "-rf", INSTALL_PATH])
run_cmd(["mv", TMP_INSTALL_PATH, INSTALL_PATH])
self._ble_progress("installing", 90)
@@ -882,6 +885,11 @@ class Setup(Widget):
# AGNOS might try to execute the installer before this process exits.
# Therefore, important to close the fd before renaming the installer.
os.close(fd)
# comma's ELF installer does `rm -rf /data/openpilot` as the comma user and asserts it
# succeeds; a prior *run* install leaves root-owned __pycache__ .pyc it can't delete, so it
# aborts before continue.sh and bounces back to setup. Clear the old tree first (sudo) so the
# installer's rm succeeds.
subprocess.run(["sudo", "rm", "-rf", INSTALL_PATH], check=False)
os.rename(tmpfile, INSTALLER_DESTINATION_PATH)
with open(INSTALLER_URL_PATH, "w") as f:

View File

@@ -51,6 +51,8 @@ INSTALLER_DESTINATION_PATH = "/tmp/installer"
INSTALLER_URL_PATH = "/tmp/installer_url"
GITHUB_FORK_URL = "https://github.com/{user}/openpilot.git"
# IQ.Pilot lives on the IQ Lvbs git server; github.com/IQLvbs is DMCA'd and dead.
GIT_URL_OVERRIDES = {"IQLvbs": "https://git.konn3kt.com/IQ.Lvbs/IQ.Pilot.git"}
CONTINUE = """#!/usr/bin/env bash
@@ -589,7 +591,7 @@ class Setup(Widget):
pass
def _fork_install_thread(self, user: str, branch: str):
git_url = GITHUB_FORK_URL.format(user=user)
git_url = GIT_URL_OVERRIDES.get(user) or GITHUB_FORK_URL.format(user=user)
label = f"{user}/{branch}"
fail_msg = "Ensure the entered URL is valid, and the device's internet connection is good."
try:
@@ -609,7 +611,8 @@ class Setup(Widget):
subprocess.run(["git", "-C", TMP_INSTALL_PATH, "submodule", "update", "--init"], check=False)
run_cmd(["rm", "-f", VALID_CACHE_PATH])
run_cmd(["rm", "-rf", INSTALL_PATH])
# sudo: a prior *run* install can leave root-owned .pyc here that a comma-user rm can't delete.
run_cmd(["sudo", "rm", "-rf", INSTALL_PATH])
run_cmd(["mv", TMP_INSTALL_PATH, INSTALL_PATH])
self._ble_progress("installing", 90)
@@ -718,6 +721,11 @@ class Setup(Widget):
# AGNOS might try to execute the installer before this process exits.
# Therefore, important to close the fd before renaming the installer.
os.close(fd)
# comma's ELF installer does `rm -rf /data/openpilot` as the comma user and asserts it
# succeeds; a prior *run* install leaves root-owned __pycache__ .pyc it can't delete, so it
# aborts before continue.sh and bounces back to setup. Clear the old tree first (sudo) so the
# installer's rm succeeds.
subprocess.run(["sudo", "rm", "-rf", INSTALL_PATH], check=False)
os.rename(tmpfile, INSTALLER_DESTINATION_PATH)
with open(INSTALLER_URL_PATH, "w") as f:

View File

@@ -406,7 +406,8 @@ class Label(Widget):
line_pos.x += width_before.x
tex = emoji_tex(emoji)
rl.draw_texture_ex(tex, line_pos, 0.0, self._font_size / tex.height * FONT_SCALE, self._text_color)
if tex is not None:
rl.draw_texture_ex(tex, line_pos, 0.0, self._font_size / tex.height * FONT_SCALE, self._text_color)
line_pos.x += self._font_size * FONT_SCALE
prev_index = end
rl.draw_text_ex(self._font, text[prev_index:], line_pos, self._font_size, 0, self._text_color)
@@ -849,8 +850,9 @@ class UnifiedLabel(Widget):
# Draw emoji
tex = emoji_tex(emoji)
emoji_scale = self._font_size / tex.height * FONT_SCALE
rl.draw_texture_ex(tex, line_pos, 0.0, emoji_scale, self._text_color)
if tex is not None:
emoji_scale = self._font_size / tex.height * FONT_SCALE
rl.draw_texture_ex(tex, line_pos, 0.0, emoji_scale, self._text_color)
# Emoji width is font_size * FONT_SCALE (as per measure_text_cached)
line_pos.x += self._font_size * FONT_SCALE
prev_index = end

View File

@@ -17,10 +17,10 @@ from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.widgets.list_view import ButtonAction, ListItem, MultipleButtonAction, ToggleAction, button_item, text_item
if gui_app.iqpilot_ui():
from openpilot.system.ui.iqpilot.widgets.list_view import button_item
from openpilot.system.ui.iqpilot.widgets.list_view import IQListItem as ListItem
from openpilot.system.ui.iqpilot.widgets.list_view import IQToggleAction as ToggleAction
from openpilot.system.ui.iqpilot.widgets.list_view import IQMultipleButtonAction as MultipleButtonAction
from openpilot.system.ui.iqwidgets.widgets.list_view import button_item
from openpilot.system.ui.iqwidgets.widgets.list_view import IQListItem as ListItem
from openpilot.system.ui.iqwidgets.widgets.list_view import IQToggleAction as ToggleAction
from openpilot.system.ui.iqwidgets.widgets.list_view import IQMultipleButtonAction as MultipleButtonAction
# These are only used for AdvancedNetworkSettings, standalone apps just need WifiManagerUI
try:
@@ -66,6 +66,7 @@ class UIState(IntEnum):
NEEDS_AUTH = 2
SHOW_FORGET_CONFIRM = 3
FORGETTING = 4
DISCONNECTING = 5
class NavButton(Widget):
@@ -328,6 +329,7 @@ class WifiManagerUI(Widget):
self._state_network: Network | None = None # for CONNECTING / NEEDS_AUTH / SHOW_FORGET_CONFIRM / FORGETTING
self._password_retry: bool = False # for NEEDS_AUTH
self.btn_width: int = 200
self.disconnect_btn_width: int = 300
self.scroll_panel = GuiScrollPanel()
self.keyboard = Keyboard(max_text_size=MAX_PASSWORD_LENGTH, min_text_size=MIN_PASSWORD_LENGTH, show_password_toggle=True)
self._load_icons()
@@ -335,6 +337,7 @@ class WifiManagerUI(Widget):
self._networks: list[Network] = []
self._networks_buttons: dict[str, Button] = {}
self._forget_networks_buttons: dict[str, Button] = {}
self._disconnect_networks_buttons: dict[str, Button] = {}
self._wifi_manager.add_callbacks(need_auth=self._on_need_auth,
activated=self._on_activated,
@@ -417,7 +420,9 @@ class WifiManagerUI(Widget):
def _draw_network_item(self, rect, network: Network):
spacing = 50
ssid_rect = rl.Rectangle(rect.x, rect.y, rect.width - self.btn_width * 2, ITEM_HEIGHT)
show_disconnect = network.is_connected and self.state not in (UIState.CONNECTING, UIState.DISCONNECTING)
reserved = self.btn_width * 2 + (self.disconnect_btn_width + spacing if show_disconnect else 0)
ssid_rect = rl.Rectangle(rect.x, rect.y, rect.width - reserved, ITEM_HEIGHT)
signal_icon_rect = rl.Rectangle(rect.x + rect.width - ICON_SIZE, rect.y + (ITEM_HEIGHT - ICON_SIZE) / 2, ICON_SIZE, ICON_SIZE)
security_icon_rect = rl.Rectangle(signal_icon_rect.x - spacing - ICON_SIZE, rect.y + (ITEM_HEIGHT - ICON_SIZE) / 2, ICON_SIZE, ICON_SIZE)
@@ -434,6 +439,10 @@ class WifiManagerUI(Widget):
if self._state_network.ssid == network.ssid:
self._networks_buttons[network.ssid].set_enabled(False)
status_text = tr("FORGETTING...")
elif self.state == UIState.DISCONNECTING and self._state_network:
if self._state_network.ssid == network.ssid:
self._networks_buttons[network.ssid].set_enabled(False)
status_text = tr("DISCONNECTING...")
elif network.security_type == SecurityType.UNSUPPORTED:
self._networks_buttons[network.ssid].set_enabled(False)
else:
@@ -455,6 +464,15 @@ class WifiManagerUI(Widget):
)
self._forget_networks_buttons[network.ssid].render(forget_btn_rect)
if show_disconnect:
disconnect_btn_rect = rl.Rectangle(
forget_btn_rect.x - self.btn_width - spacing,
forget_btn_rect.y,
self.btn_width,
80,
)
self._disconnect_networks_buttons[network.ssid].render(disconnect_btn_rect)
self._draw_status_icon(security_icon_rect, network)
self._draw_signal_strength_icon(signal_icon_rect, network)
@@ -470,6 +488,9 @@ class WifiManagerUI(Widget):
self.state = UIState.SHOW_FORGET_CONFIRM
self._state_network = network
def _disconnect_networks_buttons_callback(self, network):
self.disconnect_network(network)
def _draw_status_icon(self, rect, network: Network):
"""Draw the status icon based on network's connection state"""
icon_file = None
@@ -506,6 +527,11 @@ class WifiManagerUI(Widget):
self._state_network = network
self._wifi_manager.forget_connection(network.ssid)
def disconnect_network(self, network: Network):
self.state = UIState.DISCONNECTING
self._state_network = network
self._wifi_manager.disconnect_connection(network.ssid)
def _on_network_updated(self, networks: list[Network]):
self._networks = networks
for n in self._networks:
@@ -515,6 +541,9 @@ class WifiManagerUI(Widget):
self._forget_networks_buttons[n.ssid] = Button(tr("Forget"), partial(self._forget_networks_buttons_callback, n), button_style=ButtonStyle.FORGET_WIFI,
font_size=45)
self._forget_networks_buttons[n.ssid].set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid())
self._disconnect_networks_buttons[n.ssid] = Button(tr("Disconnect"), partial(self._disconnect_networks_buttons_callback, n),
button_style=ButtonStyle.FORGET_WIFI, font_size=45)
self._disconnect_networks_buttons[n.ssid].set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid())
def _on_need_auth(self, ssid):
network = next((n for n in self._networks if n.ssid == ssid), None)
@@ -532,7 +561,7 @@ class WifiManagerUI(Widget):
self.state = UIState.IDLE
def _on_disconnected(self):
if self.state == UIState.CONNECTING:
if self.state in (UIState.CONNECTING, UIState.DISCONNECTING):
self.state = UIState.IDLE