1
0
forked from IQ.Lvbs/IQ.Pilot
Files
2026-08-22 23:42:42 -05:00

443 lines
18 KiB
Python

"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
"""
import math
import numpy as np
import pyray as rl
from collections.abc import Callable
from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.ui.mici.widgets.stock_dialog import BigInputDialog, BigConfirmationDialog
from openpilot.selfdrive.ui.mici.widgets.stock_button import BigButton, LABEL_COLOR
from openpilot.system.ui.lib.application import gui_app, MousePos, FontWeight
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.scroller import NavScroller
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, SecurityType, normalize_ssid, wifi_network_sort_key
def _wifi_network_signature(networks: list[Network]) -> tuple[tuple[str, int, bool, int, bool], ...]:
return tuple(sorted((n.ssid, n.strength, n.is_connected, int(n.security_type), n.is_saved) for n in networks))
class LoadingAnimation(Widget):
RADIUS = 8
SPACING = 24
Y_MAG = 11.2
def __init__(self):
super().__init__()
self.set_rect(rl.Rectangle(0, 0, self.SPACING * 2 + self.RADIUS * 2, self.RADIUS * 2 + int(self.Y_MAG)))
def _render(self, _):
base_x = int(self._rect.x + self._rect.width / 2)
base_y = int(self._rect.y + self._rect.height - self.RADIUS)
for i in range(3):
x = base_x + (i - 1) * self.SPACING
y = int(base_y + min(math.sin((rl.get_time() - i * 0.2) * 4) * self.Y_MAG, 0))
alpha = int(np.interp(base_y - y, [0, self.Y_MAG], [255 * 0.45, 255 * 0.9]))
rl.draw_circle(x, y, self.RADIUS, rl.Color(255, 255, 255, alpha))
class WifiIcon(Widget):
def __init__(self, network: Network):
super().__init__()
self.set_rect(rl.Rectangle(0, 0, 48 + 5, 36 + 5))
self._wifi_slash_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 48, 42)
self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 48, 36)
self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 48, 36)
self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 48, 36)
self._lock_txt = gui_app.texture("icons_mici/settings/network/new/lock.png", 21, 27)
self._network = network
self._network_missing = False
def update_network(self, network: Network):
self._network = network
def set_network_missing(self, missing: bool):
self._network_missing = missing
def _render(self, _):
strength = round(self._network.strength / 100 * 2)
if self._network_missing:
strength_icon = self._wifi_slash_txt
elif strength == 2:
strength_icon = self._wifi_full_txt
elif strength == 1:
strength_icon = self._wifi_medium_txt
else:
strength_icon = self._wifi_low_txt
rl.draw_texture_ex(strength_icon, (self._rect.x, self._rect.y + self._rect.height - strength_icon.height), 0.0, 1.0, rl.WHITE)
if self._network.security_type not in (SecurityType.OPEN, SecurityType.UNSUPPORTED):
lock_x = self._rect.x + self._rect.width - self._lock_txt.width
lock_y = self._rect.y + self._rect.height - self._lock_txt.height + 6
rl.draw_texture_ex(self._lock_txt, (lock_x, lock_y), 0.0, 1.0, rl.WHITE)
class ForgetButton(Widget):
MARGIN = 12
def __init__(self, forget_network: Callable):
super().__init__()
self._forget_network = forget_network
self._bg_txt = gui_app.texture("icons_mici/settings/network/new/forget_button.png", 84, 84)
self._bg_pressed_txt = gui_app.texture("icons_mici/settings/network/new/forget_button_pressed.png", 84, 84)
self._trash_txt = gui_app.texture("icons_mici/settings/network/new/trash.png", 29, 35)
self.set_rect(rl.Rectangle(0, 0, 84 + self.MARGIN * 2, 84 + self.MARGIN * 2))
def _handle_mouse_release(self, mouse_pos: MousePos):
super()._handle_mouse_release(mouse_pos)
dlg = BigConfirmationDialog("slide to\nforget", gui_app.texture("icons_mici/settings/network/new/trash.png", 54, 64),
self._forget_network, red=True)
gui_app.push_widget(dlg)
def _render(self, _):
bg_txt = self._bg_pressed_txt if self.is_pressed else self._bg_txt
rl.draw_texture_ex(bg_txt, (self._rect.x + (self._rect.width - self._bg_txt.width) / 2,
self._rect.y + (self._rect.height - self._bg_txt.height) / 2), 0, 1.0, rl.WHITE)
trash_x = self._rect.x + (self._rect.width - self._trash_txt.width) / 2
trash_y = self._rect.y + (self._rect.height - self._trash_txt.height) / 2
rl.draw_texture_ex(self._trash_txt, (trash_x, trash_y), 0, 1.0, rl.WHITE)
class DisconnectButton(Widget):
MARGIN = 12
RADIUS = 42
# the only round button art is the destructive red one, and dropping a connection is not destructive
BG = rl.Color(56, 56, 61, 255)
BG_PRESSED = rl.Color(84, 84, 90, 255)
def __init__(self, disconnect_network: Callable):
super().__init__()
self._disconnect_network = disconnect_network
self._slash_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 38, 38)
self.set_rect(rl.Rectangle(0, 0, 84 + self.MARGIN * 2, 84 + self.MARGIN * 2))
def _handle_mouse_release(self, mouse_pos: MousePos):
super()._handle_mouse_release(mouse_pos)
dlg = BigConfirmationDialog("slide to\ndisconnect", gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 54, 54),
self._disconnect_network)
gui_app.push_widget(dlg)
def _render(self, _):
center = rl.Vector2(self._rect.x + self._rect.width / 2, self._rect.y + self._rect.height / 2)
rl.draw_circle_v(center, self.RADIUS, self.BG_PRESSED if self.is_pressed else self.BG)
rl.draw_texture_ex(self._slash_txt, (center.x - self._slash_txt.width / 2, center.y - self._slash_txt.height / 2),
0, 1.0, rl.WHITE)
class WifiButton(BigButton):
LABEL_PADDING = 98
LABEL_WIDTH = 402 - 98 - 28
SUB_LABEL_WIDTH = 402 - BigButton.LABEL_HORIZONTAL_PADDING * 2
def __init__(self, network: Network, wifi_manager: WifiManager, connecting_ssid: Callable[[], str | None]):
super().__init__(normalize_ssid(network.ssid), scroll=True)
self._network = network
self._wifi_manager = wifi_manager
self._connecting_ssid = connecting_ssid
self._wifi_icon = WifiIcon(network)
self._forget_btn = ForgetButton(self._forget_network)
self._disconnect_btn = DisconnectButton(self._disconnect_network)
self._check_txt = gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 32, 32)
self._network_missing = False
self._network_forgetting = False
self._network_disconnecting = False
self._wrong_password = False
@property
def network(self) -> Network:
return self._network
def update_network(self, network: Network):
self._network = network
self._wifi_icon.update_network(network)
self._network_missing = False
self._wifi_icon.set_network_missing(False)
if self._is_connected or self._is_connecting:
self._wrong_password = False
@property
def network_forgetting(self) -> bool:
return self._network_forgetting
@property
def network_missing(self) -> bool:
return self._network_missing
def _forget_network(self):
if self._network_forgetting:
return
self._network_forgetting = True
self._wifi_manager.forget_connection(self._network.ssid)
def _disconnect_network(self):
if self._network_disconnecting:
return
self._network_disconnecting = True
self._wifi_manager.disconnect_connection(self._network.ssid)
def on_forgotten(self):
self._network_forgetting = False
def on_disconnected(self):
self._network_disconnecting = False
def set_network_missing(self, missing: bool):
self._network_missing = missing
self._wifi_icon.set_network_missing(missing)
def set_wrong_password(self):
self._wrong_password = True
self.trigger_shake()
@property
def _is_saved(self) -> bool:
return self._network.is_saved
@property
def _is_connecting(self) -> bool:
return self._connecting_ssid() == self._network.ssid
@property
def _is_connected(self) -> bool:
return self._network.is_connected
@property
def _is_tethering(self) -> bool:
return getattr(self._network, "is_tethering", False)
@property
def _show_forget_btn(self) -> bool:
if self._is_tethering or self._network_forgetting or self._show_disconnect_btn:
return False
return (self._is_saved and not self._wrong_password) or self._is_connecting
@property
def _show_disconnect_btn(self) -> bool:
# 402 units of row cannot hold both buttons plus the status word, so the slot is contextual:
# disconnect while connected, forget once it is only saved
if self._is_tethering or self._network_forgetting or self._network_disconnecting:
return False
return self._is_connected
def _handle_mouse_release(self, mouse_pos: MousePos):
if self._show_forget_btn and rl.check_collision_point_rec(mouse_pos, self._forget_btn.rect):
return
if self._show_disconnect_btn and rl.check_collision_point_rec(mouse_pos, self._disconnect_btn.rect):
return
super()._handle_mouse_release(mouse_pos)
def _get_label_font_size(self):
return 48
def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None:
super().set_touch_valid_callback(lambda: touch_callback() and not self._forget_btn.is_pressed and not self._disconnect_btn.is_pressed)
self._forget_btn.set_touch_valid_callback(touch_callback)
self._disconnect_btn.set_touch_valid_callback(touch_callback)
def _update_state(self):
super()._update_state()
if any((self._network_missing, self._is_connecting, self._is_connected, self._network_forgetting,
self._network_disconnecting, self._network.security_type == SecurityType.UNSUPPORTED)):
self.set_enabled(False)
self._sub_label.set_color(rl.Color(255, 255, 255, int(255 * 0.585)))
self._sub_label.set_font_weight(FontWeight.ROMAN)
if self._network_forgetting:
self.set_value("forgetting...")
elif self._network_disconnecting:
self.set_value("disconnecting...")
elif self._is_connecting:
self.set_value("starting..." if self._is_tethering else "connecting...")
elif self._is_connected:
self.set_value("tethering" if self._is_tethering else "connected")
elif self._network_missing:
self.set_value("not in range")
else:
self.set_value("unsupported")
else:
self.set_value("wrong password" if self._wrong_password else "connect")
self.set_enabled(True)
self._sub_label.set_color(rl.Color(255, 255, 255, int(255 * 0.9)))
self._sub_label.set_font_weight(FontWeight.SEMI_BOLD)
def _draw_content(self, btn_x: float, btn_y: float, btn_width: float, btn_height: float):
self._label.set_color(LABEL_COLOR)
label_rect = rl.Rectangle(btn_x + self.LABEL_PADDING, btn_y + self.LABEL_VERTICAL_PADDING,
self.LABEL_WIDTH, btn_height - self.LABEL_VERTICAL_PADDING * 2)
self._label.render(label_rect)
if self.value:
sub_label_x = self._rect.x + self.LABEL_HORIZONTAL_PADDING
label_y = btn_y + self._rect.height - self.LABEL_VERTICAL_PADDING
sub_label_w = self.SUB_LABEL_WIDTH - (self._forget_btn.rect.width if self._show_forget_btn else 0) \
- (self._disconnect_btn.rect.width if self._show_disconnect_btn else 0)
sub_label_height = self._sub_label.get_content_height(sub_label_w)
if self._is_connected and not self._network_forgetting and not self._network_disconnecting:
check_y = int(label_y - sub_label_height + (sub_label_height - self._check_txt.height) / 2)
rl.draw_texture_ex(self._check_txt, rl.Vector2(sub_label_x, check_y), 0.0, 1.0, rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)))
sub_label_x += self._check_txt.width + 14
sub_label_rect = rl.Rectangle(sub_label_x, label_y - sub_label_height, sub_label_w, sub_label_height)
self._sub_label.render(sub_label_rect)
self._wifi_icon.render(rl.Rectangle(self._rect.x + 30, btn_y + 30, self._wifi_icon.rect.width, self._wifi_icon.rect.height))
btn_right = self._rect.x + self._rect.width
if self._show_forget_btn:
self._forget_btn.render(rl.Rectangle(
btn_right - self._forget_btn.rect.width,
btn_y + self._rect.height - self._forget_btn.rect.height,
self._forget_btn.rect.width, self._forget_btn.rect.height))
btn_right -= self._forget_btn.rect.width
if self._show_disconnect_btn:
self._disconnect_btn.render(rl.Rectangle(
btn_right - self._disconnect_btn.rect.width,
btn_y + self._rect.height - self._disconnect_btn.rect.height,
self._disconnect_btn.rect.width, self._disconnect_btn.rect.height))
class ScanningButton(BigButton):
def __init__(self, is_scanning: Callable[[], bool]):
super().__init__("", "searching for networks")
self.set_enabled(False)
self._loading_animation = LoadingAnimation()
self._is_scanning = is_scanning
def _draw_content(self, btn_x: float, btn_y: float, btn_width: float, btn_height: float):
super()._draw_content(btn_x, btn_y, btn_width, btn_height)
if not self._is_scanning():
return
anim = self._loading_animation
anim.set_position(btn_x + btn_width - anim.rect.width - 40, btn_y + btn_height - anim.rect.height - 30)
anim.render()
class WifiUIMici(NavScroller):
CALLBACK_INTERVAL_FRAMES = 3
def __init__(self, wifi_manager: WifiManager):
super().__init__()
self._wifi_manager = wifi_manager
self._scanning_btn = ScanningButton(lambda: self._wifi_manager.is_scanning)
self._networks: dict[str, Network] = {}
self._network_signature: tuple[tuple[str, int, bool, int, bool], ...] = ()
self._connecting: str | None = None
self._callback_frame = 0
self._wifi_manager.add_callbacks(
need_auth=self._on_need_auth,
activated=self._on_activated,
forgotten=self._on_forgotten,
networks_updated=self._on_network_updated,
disconnected=self._on_disconnected,
)
def _connecting_ssid(self) -> str | None:
return self._connecting
def show_event(self):
super().show_event()
self._wifi_manager.set_active(True)
self._callback_frame = 0
self._update_buttons(re_sort=True)
def hide_event(self):
super().hide_event()
self._wifi_manager.set_active(False)
def _on_network_updated(self, networks: list[Network]):
signature = _wifi_network_signature(networks)
if signature == self._network_signature:
return
self._network_signature = signature
self._networks = {n.ssid: n for n in networks}
self._update_buttons()
def _on_activated(self):
self._connecting = None
def _on_disconnected(self):
self._connecting = None
for btn in self._scroller.items:
if isinstance(btn, WifiButton):
btn.on_disconnected()
def _on_forgotten(self, ssid=None):
self._connecting = None
for btn in self._scroller.items:
if isinstance(btn, WifiButton) and (ssid is None or btn.network.ssid == ssid):
btn.on_forgotten()
def _update_buttons(self, re_sort: bool = False):
sorted_networks = sorted(self._networks.values(), key=wifi_network_sort_key)
existing = {btn.network.ssid: btn for btn in self._scroller.items if isinstance(btn, WifiButton)}
for network in sorted_networks:
if network.ssid in existing:
existing[network.ssid].update_network(network)
else:
btn = WifiButton(network, self._wifi_manager, self._connecting_ssid)
btn.set_click_callback(lambda ssid=network.ssid: self._connect_to_network(ssid))
self._scroller.add_widget(btn)
current_ssids = set(self._networks)
for btn in self._scroller.items:
if isinstance(btn, WifiButton):
btn.set_network_missing(btn.network.ssid not in current_ssids)
if re_sort or sorted_networks:
order = {btn.network.ssid: idx for idx, btn in enumerate(self._scroller.items) if isinstance(btn, WifiButton)}
wifi_buttons = [btn for btn in self._scroller.items if isinstance(btn, WifiButton)]
other_items = [btn for btn in self._scroller.items if not isinstance(btn, WifiButton) and btn is not self._scanning_btn]
wifi_buttons.sort(key=lambda btn: (*wifi_network_sort_key(btn.network, btn.network_missing), order[btn.network.ssid]))
self._scroller.items[:] = [*wifi_buttons, *other_items]
items = self._scroller.items
if self._scanning_btn in items:
items.append(items.pop(items.index(self._scanning_btn)))
else:
self._scroller.add_widget(self._scanning_btn)
def _connect_with_password(self, ssid: str, password: str):
self._connecting = ssid
self._wifi_manager.connect_to_network(ssid, password)
self._move_network_to_front(ssid)
def _connect_to_network(self, ssid: str):
network = self._networks.get(ssid)
if network is None:
cloudlog.warning(f"Trying to connect to unknown network: {ssid}")
return
if network.is_saved:
self._connecting = ssid
self._wifi_manager.activate_connection(ssid)
elif network.security_type == SecurityType.OPEN:
self._connecting = ssid
self._wifi_manager.connect_to_network(ssid, "")
else:
self._on_need_auth(ssid, False)
return
self._move_network_to_front(ssid)
def _on_need_auth(self, ssid, incorrect_password=True):
if incorrect_password:
self._connecting = None
for btn in self._scroller.items:
if isinstance(btn, WifiButton) and btn.network.ssid == ssid:
btn.set_wrong_password()
break
return
dlg = BigInputDialog("enter password...", "", minimum_length=8,
confirm_callback=lambda _password: self._connect_with_password(ssid, _password))
gui_app.push_widget(dlg)
def _move_network_to_front(self, ssid: str | None):
idx = next((i for i, btn in enumerate(self._scroller.items)
if isinstance(btn, WifiButton) and btn.network.ssid == ssid), None) if ssid else None
if idx is not None and idx > 0:
self._scroller.move_item(idx, 0)
def _update_state(self):
super()._update_state()
if self._callback_frame % self.CALLBACK_INTERVAL_FRAMES == 0:
self._wifi_manager.process_callbacks()
self._callback_frame += 1