forked from IQ.Lvbs/IQ.Pilot
443 lines
18 KiB
Python
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
|