IQ.Pilot Release Commit @ 0798119
This commit is contained in:
3
selfdrive/ui/mici/layouts/settings/network/__init__.py
Normal file
3
selfdrive/ui/mici/layouts/settings/network/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
187
selfdrive/ui/mici/layouts/settings/network/esim_ui.py
Normal file
187
selfdrive/ui/mici/layouts/settings/network/esim_ui.py
Normal file
@@ -0,0 +1,187 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.system.hardware.base import Profile
|
||||
from openpilot.system.hardware.tici.esim_manager import EsimManager, EsimUiState, get_esim_manager
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets import DialogResult, NavWidget
|
||||
from openpilot.system.ui.widgets.esim_scanner import EsimQrScannerDialog
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import NeonBigButton
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigInputDialog, BigMultiOptionDialog, BigConfirmationDialogV2
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
|
||||
|
||||
class EsimUIMici(NavWidget):
|
||||
def __init__(self, back_callback: Callable):
|
||||
super().__init__()
|
||||
self._manager: EsimManager = get_esim_manager()
|
||||
self._state = EsimUiState()
|
||||
self._callback_registered = False
|
||||
self._scroller = Scroller([], snap_items=False)
|
||||
self._rebuild_scroller()
|
||||
self.set_back_callback(back_callback)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._scroller.show_event()
|
||||
if not self._callback_registered:
|
||||
self._manager.add_callback(self._on_state_update)
|
||||
self._callback_registered = True
|
||||
self._manager.refresh_profiles()
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
if self._callback_registered:
|
||||
self._manager.remove_callback(self._on_state_update)
|
||||
self._callback_registered = False
|
||||
|
||||
def _is_busy(self) -> bool:
|
||||
return self._state.busy
|
||||
|
||||
def _status_text(self) -> str:
|
||||
if self._state.message:
|
||||
return self._state.message
|
||||
return self._state.state.value
|
||||
|
||||
def _on_state_update(self, state: EsimUiState):
|
||||
self._state = state
|
||||
self._rebuild_scroller()
|
||||
|
||||
def _show_choice_dialog(self, title: str, options: list[str], callback: Callable[[str], None]) -> None:
|
||||
if not options:
|
||||
return
|
||||
dlg = BigMultiOptionDialog(
|
||||
options,
|
||||
options[0],
|
||||
right_btn="check",
|
||||
right_btn_callback=lambda: callback(dlg.get_selected_option()),
|
||||
)
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
|
||||
def _rebuild_scroller(self):
|
||||
widgets = []
|
||||
|
||||
status_btn = NeonBigButton("status", chips=[self._status_text()])
|
||||
status_btn.set_enabled(False)
|
||||
widgets.append(status_btn)
|
||||
|
||||
refresh_btn = NeonBigButton("refresh profiles")
|
||||
refresh_btn.set_enabled(lambda: not self._is_busy())
|
||||
refresh_btn.set_click_callback(lambda: self._manager.refresh_profiles())
|
||||
widgets.append(refresh_btn)
|
||||
|
||||
add_btn = NeonBigButton("add profile", chips=["scan qr / enter code"])
|
||||
add_btn.set_enabled(lambda: not self._is_busy())
|
||||
add_btn.set_click_callback(self._on_add_profile)
|
||||
widgets.append(add_btn)
|
||||
|
||||
profiles = self._state.profiles or []
|
||||
for p in profiles:
|
||||
widgets.append(self._make_profile_button(p))
|
||||
|
||||
self._scroller = Scroller(widgets, snap_items=False)
|
||||
|
||||
def _make_profile_button(self, profile: Profile) -> NeonBigButton:
|
||||
title = profile.nickname if profile.nickname else profile.iccid
|
||||
value = f"{profile.provider or 'provider unknown'}{' • active' if profile.enabled else ''}"
|
||||
btn = NeonBigButton(title, chips=[value])
|
||||
btn.set_enabled(lambda: not self._is_busy())
|
||||
btn.set_click_callback(lambda profile=profile: self._on_profile_selected(profile))
|
||||
return btn
|
||||
|
||||
def _on_add_profile(self):
|
||||
options = ["scan qr", "enter code"]
|
||||
|
||||
def _selected(option: str):
|
||||
if option == "scan qr":
|
||||
self._scan_qr()
|
||||
elif option == "enter code":
|
||||
self._manual_entry()
|
||||
|
||||
self._show_choice_dialog("add esim profile", options, _selected)
|
||||
|
||||
def _scan_qr(self):
|
||||
scanner = EsimQrScannerDialog()
|
||||
self._manager.set_scanning_state(True)
|
||||
|
||||
def _done(result: int):
|
||||
self._manager.set_scanning_state(False)
|
||||
if result != DialogResult.CONFIRM or not scanner.code:
|
||||
return
|
||||
self._prompt_nickname_and_add(scanner.code)
|
||||
|
||||
gui_app.set_modal_overlay(scanner, _done)
|
||||
|
||||
def _manual_entry(self):
|
||||
dlg = BigInputDialog("enter LPA activation code...", "", minimum_length=1,
|
||||
confirm_callback=lambda code: self._prompt_nickname_and_add(code.strip()) if code.strip() else None)
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
|
||||
def _prompt_nickname_and_add(self, code: str):
|
||||
dlg = BigInputDialog("optional nickname...", "", minimum_length=0,
|
||||
confirm_callback=lambda nickname: self._manager.add_profile(code, nickname.strip() or None))
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
|
||||
def _on_profile_selected(self, profile: Profile):
|
||||
options = []
|
||||
if not profile.enabled:
|
||||
options.append("activate")
|
||||
options.append("rename")
|
||||
if self._manager.is_comma_profile(profile.iccid):
|
||||
options.append("remove comma psim")
|
||||
elif not profile.enabled:
|
||||
options.append("delete")
|
||||
|
||||
def _selected(option: str):
|
||||
if option == "activate":
|
||||
self._manager.switch_profile(profile.iccid)
|
||||
elif option == "rename":
|
||||
self._rename_profile(profile)
|
||||
elif option == "remove comma psim":
|
||||
self._remove_comma_profile()
|
||||
elif option == "delete":
|
||||
self._manager.delete_profile(profile.iccid)
|
||||
|
||||
self._show_choice_dialog("profile actions", options, _selected)
|
||||
|
||||
def _rename_profile(self, profile: Profile):
|
||||
dlg = BigInputDialog("rename profile...", profile.nickname or "", minimum_length=1,
|
||||
confirm_callback=lambda nickname: self._manager.rename_profile(profile.iccid, nickname.strip()) if nickname.strip() else None)
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
|
||||
def _remove_comma_profile(self):
|
||||
dlg = BigDialog(
|
||||
"Warning",
|
||||
"This will permanently wipe the Comma pSIM profile from the SIM.",
|
||||
right_btn="check",
|
||||
right_btn_callback=self._remove_comma_profile_final_warning,
|
||||
)
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
|
||||
def _remove_comma_profile_final_warning(self):
|
||||
dlg = BigDialog(
|
||||
"Final Warning",
|
||||
"You must use your own eSIM profile after this. You cannot use Comma Prime again unless you buy a new SIM from comma.",
|
||||
right_btn="check",
|
||||
right_btn_callback=self._confirm_remove_comma_profile,
|
||||
)
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
|
||||
def _confirm_remove_comma_profile(self):
|
||||
dlg = BigConfirmationDialogV2(
|
||||
"slide to remove\ncomma psim",
|
||||
"icons_mici/settings/network/new/trash.png",
|
||||
red=True,
|
||||
confirm_callback=self._manager.bootstrap,
|
||||
)
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
if not self._manager.is_supported():
|
||||
from openpilot.system.ui.widgets.label import gui_label
|
||||
gui_label(rect, "Insert the original comma SIM card that came with the device to use eSIM", 48, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||
return
|
||||
self._scroller.render(rect)
|
||||
230
selfdrive/ui/mici/layouts/settings/network/network_layout.py
Normal file
230
selfdrive/ui/mici/layouts/settings/network/network_layout.py
Normal file
@@ -0,0 +1,230 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
import pyray as rl
|
||||
from enum import IntEnum
|
||||
from collections.abc import Callable
|
||||
|
||||
from openpilot.system.ui.widgets.scroller import Scroller, draw_scroller_edge_fades, draw_scroller_page_slider
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.network.esim_ui import EsimUIMici
|
||||
from openpilot.selfdrive.ui.mici.widgets.stock_button import BigButton, BigParamControl, BigMultiToggle
|
||||
from openpilot.selfdrive.ui.mici.widgets.stock_dialog import BigInputDialog
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets.nav_widget import NavWidget
|
||||
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, MeteredType
|
||||
from openpilot.system.hardware.tici.esim_manager import get_esim_manager
|
||||
|
||||
|
||||
class NetworkPanelType(IntEnum):
|
||||
NONE = 0
|
||||
WIFI = 1
|
||||
ESIM = 2
|
||||
|
||||
|
||||
class NetworkLayoutMici(NavWidget):
|
||||
CALLBACK_INTERVAL_FRAMES = 3
|
||||
|
||||
def __init__(self, back_callback: Callable):
|
||||
super().__init__()
|
||||
|
||||
self._current_panel = NetworkPanelType.WIFI
|
||||
self._callback_frame = 0
|
||||
self._esim_profile_count: str | None = None
|
||||
self._esim_profile_frame = 0
|
||||
|
||||
self._wifi_manager = WifiManager()
|
||||
self._wifi_manager.set_active(False)
|
||||
self._wifi_ui = WifiUIMici(self._wifi_manager)
|
||||
self._esim_ui = EsimUIMici(back_callback=lambda: self._switch_to_panel(NetworkPanelType.NONE))
|
||||
self._esim_manager = get_esim_manager()
|
||||
|
||||
self._wifi_manager.add_callbacks(
|
||||
networks_updated=self._on_network_updated,
|
||||
)
|
||||
|
||||
# ******** Tethering ********
|
||||
def tethering_toggle_callback(checked: bool):
|
||||
self._tethering_toggle_btn.set_enabled(False)
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
self._wifi_manager.set_tethering_active(checked)
|
||||
|
||||
self._tethering_checked = False
|
||||
self._tethering_toggle_btn = BigButton("tethering", "disabled")
|
||||
self._tethering_toggle_btn.set_click_callback(lambda: self._on_tethering_clicked(tethering_toggle_callback))
|
||||
|
||||
def tethering_password_callback(password: str):
|
||||
if password:
|
||||
self._wifi_manager.set_tethering_password(password)
|
||||
|
||||
def tethering_password_clicked():
|
||||
tethering_password = self._wifi_manager.tethering_password
|
||||
dlg = BigInputDialog("enter password...", tethering_password, minimum_length=8,
|
||||
confirm_callback=tethering_password_callback)
|
||||
gui_app.push_widget(dlg)
|
||||
|
||||
self._tethering_password_btn = BigButton("tethering password")
|
||||
self._tethering_password_btn.set_click_callback(tethering_password_clicked)
|
||||
|
||||
# ******** IP Address ********
|
||||
self._ip_address_btn = BigButton("IP Address", "Not connected")
|
||||
|
||||
# ******** Network Metered ********
|
||||
def network_metered_callback(value: str):
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
metered = {
|
||||
'default': MeteredType.UNKNOWN,
|
||||
'metered': MeteredType.YES,
|
||||
'unmetered': MeteredType.NO
|
||||
}.get(value, MeteredType.UNKNOWN)
|
||||
self._wifi_manager.set_current_network_metered(metered)
|
||||
|
||||
# TODO: signal for current network metered type when changing networks, this is wrong until you press it once
|
||||
# TODO: disable when not connected
|
||||
self._network_metered_btn = BigMultiToggle("network usage", ["default", "metered", "unmetered"], select_callback=network_metered_callback)
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
|
||||
wifi_button = BigButton("wi-fi")
|
||||
wifi_button.set_click_callback(lambda: gui_app.push_widget(self._wifi_ui))
|
||||
self._esim_button = BigButton("eSIM", "manage profiles")
|
||||
self._esim_button.set_click_callback(lambda: self._switch_to_panel(NetworkPanelType.ESIM))
|
||||
self._esim_button.set_visible(lambda: self._esim_manager.is_supported())
|
||||
|
||||
# ******** Advanced settings ********
|
||||
# ******** Roaming toggle ********
|
||||
self._roaming_btn = BigParamControl("enable roaming", "GsmRoaming", toggle_callback=self._toggle_roaming)
|
||||
|
||||
# ******** APN settings ********
|
||||
self._apn_btn = BigButton("apn settings")
|
||||
self._apn_btn.set_click_callback(self._edit_apn)
|
||||
|
||||
# ******** Cellular metered toggle ********
|
||||
self._cellular_metered_btn = BigParamControl("cellular metered", "GsmMetered", toggle_callback=self._toggle_cellular_metered)
|
||||
|
||||
# Main scroller ----------------------------------
|
||||
self._scroller = Scroller([
|
||||
wifi_button,
|
||||
self._esim_button,
|
||||
self._network_metered_btn,
|
||||
self._tethering_toggle_btn,
|
||||
self._tethering_password_btn,
|
||||
# /* Advanced settings
|
||||
self._roaming_btn,
|
||||
self._apn_btn,
|
||||
self._cellular_metered_btn,
|
||||
# */
|
||||
self._ip_address_btn,
|
||||
], snap_items=False)
|
||||
|
||||
# Set initial config
|
||||
roaming_enabled = ui_state.params.get_bool("GsmRoaming")
|
||||
metered = ui_state.params.get_bool("GsmMetered")
|
||||
self._wifi_manager.update_gsm_settings(roaming_enabled, ui_state.params.get("GsmApn") or "", metered)
|
||||
|
||||
# Set up back navigation
|
||||
self.set_back_callback(back_callback)
|
||||
|
||||
def _back_enabled(self) -> bool:
|
||||
# Only allow swipe-to-dismiss back to settings when no sub-panel (eSIM) is open.
|
||||
return self._current_panel == NetworkPanelType.NONE
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
# konn3kt has no managed cellular SIM, so always expose the GSM/APN settings.
|
||||
show_cell_settings = True
|
||||
self._wifi_manager.set_ipv4_forward(show_cell_settings)
|
||||
self._roaming_btn.set_visible(show_cell_settings)
|
||||
self._apn_btn.set_visible(show_cell_settings)
|
||||
self._cellular_metered_btn.set_visible(show_cell_settings)
|
||||
|
||||
self._esim_profile_frame += 1
|
||||
if self._esim_profile_frame % 30 == 0:
|
||||
esim_profiles = (self._esim_manager.get_state().profiles or []) if self._esim_manager.is_supported() else []
|
||||
count = f"{len(esim_profiles)} profiles"
|
||||
if count != self._esim_profile_count:
|
||||
self._esim_profile_count = count
|
||||
self._esim_button.set_value(count)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._current_panel = NetworkPanelType.NONE
|
||||
self._esim_profile_frame = 0
|
||||
self._esim_profile_count = None
|
||||
self._roaming_btn.refresh()
|
||||
self._cellular_metered_btn.refresh()
|
||||
self._scroller.show_event()
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
if self._current_panel == NetworkPanelType.ESIM:
|
||||
self._esim_ui.hide_event()
|
||||
|
||||
def _toggle_roaming(self, checked: bool):
|
||||
self._wifi_manager.update_gsm_settings(checked, ui_state.params.get("GsmApn") or "", ui_state.params.get_bool("GsmMetered"))
|
||||
|
||||
def _edit_apn(self):
|
||||
def update_apn(apn: str):
|
||||
apn = apn.strip()
|
||||
if apn == "":
|
||||
ui_state.params.remove("GsmApn")
|
||||
else:
|
||||
ui_state.params.put("GsmApn", apn)
|
||||
|
||||
self._wifi_manager.update_gsm_settings(ui_state.params.get_bool("GsmRoaming"), apn, ui_state.params.get_bool("GsmMetered"))
|
||||
|
||||
current_apn = ui_state.params.get("GsmApn") or ""
|
||||
dlg = BigInputDialog("enter APN", current_apn, minimum_length=0, confirm_callback=update_apn)
|
||||
gui_app.push_widget(dlg)
|
||||
|
||||
def _toggle_cellular_metered(self, checked: bool):
|
||||
self._wifi_manager.update_gsm_settings(ui_state.params.get_bool("GsmRoaming"), ui_state.params.get("GsmApn") or "", checked)
|
||||
|
||||
def _on_tethering_clicked(self, toggle_callback):
|
||||
self._tethering_checked = not self._tethering_checked
|
||||
self._tethering_toggle_btn.set_value("enabled" if self._tethering_checked else "disabled")
|
||||
toggle_callback(self._tethering_checked)
|
||||
|
||||
def _on_network_updated(self, networks: list[Network]):
|
||||
# Update tethering state
|
||||
tethering_active = self._wifi_manager.is_tethering_active()
|
||||
self._tethering_toggle_btn.set_enabled(True)
|
||||
self._network_metered_btn.set_enabled(lambda: not tethering_active and bool(self._wifi_manager.ipv4_address))
|
||||
self._tethering_checked = tethering_active
|
||||
self._tethering_toggle_btn.set_value("enabled" if tethering_active else "disabled")
|
||||
|
||||
# Update IP address
|
||||
self._ip_address_btn.set_value(self._wifi_manager.ipv4_address or "Not connected")
|
||||
|
||||
# Update network metered
|
||||
self._network_metered_btn.set_value(
|
||||
{
|
||||
MeteredType.UNKNOWN: 'default',
|
||||
MeteredType.YES: 'metered',
|
||||
MeteredType.NO: 'unmetered'
|
||||
}.get(self._wifi_manager.current_network_metered, 'default'))
|
||||
|
||||
def _switch_to_panel(self, panel_type: NetworkPanelType):
|
||||
if panel_type == NetworkPanelType.ESIM:
|
||||
if not self._esim_manager.is_supported():
|
||||
return
|
||||
self._esim_ui.show_event()
|
||||
elif self._current_panel == NetworkPanelType.ESIM:
|
||||
self._esim_ui.hide_event()
|
||||
|
||||
self._current_panel = panel_type
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
rl.draw_rectangle_rec(rect, rl.BLACK)
|
||||
if self._callback_frame % self.CALLBACK_INTERVAL_FRAMES == 0:
|
||||
self._wifi_manager.process_callbacks()
|
||||
self._callback_frame += 1
|
||||
|
||||
if self._current_panel == NetworkPanelType.ESIM:
|
||||
self._esim_ui.render(rect)
|
||||
else:
|
||||
self._scroller.render(rect)
|
||||
draw_scroller_edge_fades(rect)
|
||||
draw_scroller_page_slider(self._scroller, rect)
|
||||
36
selfdrive/ui/mici/layouts/settings/network/test_wifi_sort.py
Normal file
36
selfdrive/ui/mici/layouts/settings/network/test_wifi_sort.py
Normal file
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Small standalone check for the mici WiFi menu sort order.
|
||||
|
||||
Run with:
|
||||
uv run python selfdrive/ui/mici/layouts/settings/network/test_wifi_sort.py
|
||||
"""
|
||||
|
||||
from openpilot.system.ui.lib.wifi_manager import Network, SecurityType, wifi_network_sort_key
|
||||
|
||||
|
||||
def _network(ssid: str, strength: int, connected: bool = False) -> Network:
|
||||
return Network(ssid, strength, connected, SecurityType.WPA2, True)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
cafe = _network("cafe", 35)
|
||||
home = _network("home", 75)
|
||||
connected = _network("connected", 20, connected=True)
|
||||
missing_saved = _network("missing-saved", 90)
|
||||
zero_strength = _network("zero-strength", 0)
|
||||
|
||||
entries = [
|
||||
(cafe, False),
|
||||
(missing_saved, True),
|
||||
(zero_strength, False),
|
||||
(connected, False),
|
||||
(home, False),
|
||||
]
|
||||
|
||||
ordered = [network.ssid for network, missing in sorted(entries, key=lambda entry: wifi_network_sort_key(*entry))]
|
||||
assert ordered == ["connected", "home", "cafe", "missing-saved", "zero-strength"], ordered
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
442
selfdrive/ui/mici/layouts/settings/network/wifi_ui.py
Normal file
442
selfdrive/ui/mici/layouts/settings/network/wifi_ui.py
Normal file
@@ -0,0 +1,442 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user