raylib: networking parity with QT (#36197)

* match style

* all this was not naught

* cool can do this

* fix toggle callback - also not for naught

* always process callbacks

* toggle stuff

* cleaner

* tethering password

* clean up

* todos for later

* this is fineee

* add metered options

* wifi metered button

* add hidden network buutton and fix instant modal to modal

* damped filter

* Revert "damped filter"

This reverts commit f9f98d5d70.

* fix metered toggle when disconnected

* fix tethering enabled

* ohh

* fix keyboard title

* disable edit button temp

* move here

* proper disable

* clean up

* more

* move for loop into enqueue function

* flippy

* got more :(

* todo

* clean up

* mypy

* rename

* todo

* rename

* again

* again

* format
pull/36203/head
Shane Smiskol 2 days ago committed by GitHub
parent 56c49b3b42
commit 1ca9fe35c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      selfdrive/ui/layouts/settings/settings.py
  2. 4
      selfdrive/ui/layouts/sidebar.py
  3. 8
      system/ui/lib/application.py
  4. 4
      system/ui/lib/networkmanager.py
  5. 269
      system/ui/lib/wifi_manager.py
  6. 20
      system/ui/widgets/button.py
  7. 7
      system/ui/widgets/keyboard.py
  8. 23
      system/ui/widgets/list_view.py
  9. 201
      system/ui/widgets/network.py
  10. 7
      system/ui/widgets/toggle.py

@ -11,7 +11,7 @@ from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.wifi_manager import WifiManager from openpilot.system.ui.lib.wifi_manager import WifiManager
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.network import WifiManagerUI from openpilot.system.ui.widgets.network import NetworkUI
# Settings close button # Settings close button
SETTINGS_CLOSE_TEXT = "×" SETTINGS_CLOSE_TEXT = "×"
@ -59,7 +59,7 @@ class SettingsLayout(Widget):
self._panels = { self._panels = {
PanelType.DEVICE: PanelInfo("Device", DeviceLayout()), PanelType.DEVICE: PanelInfo("Device", DeviceLayout()),
PanelType.NETWORK: PanelInfo("Network", WifiManagerUI(wifi_manager)), PanelType.NETWORK: PanelInfo("Network", NetworkUI(wifi_manager)),
PanelType.TOGGLES: PanelInfo("Toggles", TogglesLayout()), PanelType.TOGGLES: PanelInfo("Toggles", TogglesLayout()),
PanelType.SOFTWARE: PanelInfo("Software", SoftwareLayout()), PanelType.SOFTWARE: PanelInfo("Software", SoftwareLayout()),
PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayout()), PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayout()),

@ -189,11 +189,11 @@ class Sidebar(Widget):
# Draw colored left edge (clipped rounded rectangle) # Draw colored left edge (clipped rounded rectangle)
edge_rect = rl.Rectangle(metric_rect.x + 4, metric_rect.y + 4, 100, 118) edge_rect = rl.Rectangle(metric_rect.x + 4, metric_rect.y + 4, 100, 118)
rl.begin_scissor_mode(int(metric_rect.x + 4), int(metric_rect.y), 18, int(metric_rect.height)) rl.begin_scissor_mode(int(metric_rect.x + 4), int(metric_rect.y), 18, int(metric_rect.height))
rl.draw_rectangle_rounded(edge_rect, 0.18, 10, metric.color) rl.draw_rectangle_rounded(edge_rect, 0.3, 10, metric.color)
rl.end_scissor_mode() rl.end_scissor_mode()
# Draw border # Draw border
rl.draw_rectangle_rounded_lines_ex(metric_rect, 0.15, 10, 2, Colors.METRIC_BORDER) rl.draw_rectangle_rounded_lines_ex(metric_rect, 0.3, 10, 2, Colors.METRIC_BORDER)
# Draw label and value # Draw label and value
labels = [metric.label, metric.value] labels = [metric.label, metric.value]

@ -269,11 +269,11 @@ class GuiApplication:
raise Exception raise Exception
if result >= 0: if result >= 0:
# Execute callback with the result and clear the overlay # Clear the overlay and execute the callback
if self._modal_overlay.callback is not None: original_modal = self._modal_overlay
self._modal_overlay.callback(result)
self._modal_overlay = ModalOverlay() self._modal_overlay = ModalOverlay()
if original_modal.callback is not None:
original_modal.callback(result)
else: else:
yield yield

@ -21,9 +21,11 @@ NM_ACCESS_POINT_IFACE = 'org.freedesktop.NetworkManager.AccessPoint'
NM_SETTINGS_PATH = '/org/freedesktop/NetworkManager/Settings' NM_SETTINGS_PATH = '/org/freedesktop/NetworkManager/Settings'
NM_SETTINGS_IFACE = 'org.freedesktop.NetworkManager.Settings' NM_SETTINGS_IFACE = 'org.freedesktop.NetworkManager.Settings'
NM_CONNECTION_IFACE = 'org.freedesktop.NetworkManager.Settings.Connection' NM_CONNECTION_IFACE = 'org.freedesktop.NetworkManager.Settings.Connection'
NM_ACTIVE_CONNECTION_IFACE = 'org.freedesktop.NetworkManager.Connection.Active'
NM_WIRELESS_IFACE = 'org.freedesktop.NetworkManager.Device.Wireless' NM_WIRELESS_IFACE = 'org.freedesktop.NetworkManager.Device.Wireless'
NM_PROPERTIES_IFACE = 'org.freedesktop.DBus.Properties' NM_PROPERTIES_IFACE = 'org.freedesktop.DBus.Properties'
NM_DEVICE_IFACE = "org.freedesktop.NetworkManager.Device" NM_DEVICE_IFACE = 'org.freedesktop.NetworkManager.Device'
NM_IP4_CONFIG_IFACE = 'org.freedesktop.NetworkManager.IP4Config'
NM_DEVICE_TYPE_WIFI = 2 NM_DEVICE_TYPE_WIFI = 2
NM_DEVICE_TYPE_MODEM = 8 NM_DEVICE_TYPE_MODEM = 8

@ -15,6 +15,7 @@ from jeepney.low_level import MessageType
from jeepney.wrappers import Properties from jeepney.wrappers import Properties
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.common.params import Params
from openpilot.system.ui.lib.networkmanager import (NM, NM_WIRELESS_IFACE, NM_802_11_AP_SEC_PAIR_WEP40, from openpilot.system.ui.lib.networkmanager import (NM, NM_WIRELESS_IFACE, NM_802_11_AP_SEC_PAIR_WEP40,
NM_802_11_AP_SEC_PAIR_WEP104, NM_802_11_AP_SEC_GROUP_WEP40, NM_802_11_AP_SEC_PAIR_WEP104, NM_802_11_AP_SEC_GROUP_WEP40,
NM_802_11_AP_SEC_GROUP_WEP104, NM_802_11_AP_SEC_KEY_MGMT_PSK, NM_802_11_AP_SEC_GROUP_WEP104, NM_802_11_AP_SEC_KEY_MGMT_PSK,
@ -23,8 +24,8 @@ from openpilot.system.ui.lib.networkmanager import (NM, NM_WIRELESS_IFACE, NM_80
NM_PATH, NM_IFACE, NM_ACCESS_POINT_IFACE, NM_SETTINGS_PATH, NM_PATH, NM_IFACE, NM_ACCESS_POINT_IFACE, NM_SETTINGS_PATH,
NM_SETTINGS_IFACE, NM_CONNECTION_IFACE, NM_DEVICE_IFACE, NM_SETTINGS_IFACE, NM_CONNECTION_IFACE, NM_DEVICE_IFACE,
NM_DEVICE_TYPE_WIFI, NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT, NM_DEVICE_TYPE_WIFI, NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT,
NM_DEVICE_STATE_REASON_NEW_ACTIVATION, NM_DEVICE_STATE_REASON_NEW_ACTIVATION, NM_ACTIVE_CONNECTION_IFACE,
NMDeviceState) NM_IP4_CONFIG_IFACE, NMDeviceState)
TETHERING_IP_ADDRESS = "192.168.43.1" TETHERING_IP_ADDRESS = "192.168.43.1"
DEFAULT_TETHERING_PASSWORD = "swagswagcomma" DEFAULT_TETHERING_PASSWORD = "swagswagcomma"
@ -40,6 +41,12 @@ class SecurityType(IntEnum):
UNSUPPORTED = 4 UNSUPPORTED = 4
class MeteredType(IntEnum):
UNKNOWN = 0
YES = 1
NO = 2
def get_security_type(flags: int, wpa_flags: int, rsn_flags: int) -> SecurityType: def get_security_type(flags: int, wpa_flags: int, rsn_flags: int) -> SecurityType:
wpa_props = wpa_flags | rsn_flags wpa_props = wpa_flags | rsn_flags
@ -114,7 +121,7 @@ class AccessPoint:
class WifiManager: class WifiManager:
def __init__(self): def __init__(self):
self._networks = [] # a network can be comprised of multiple APs self._networks: list[Network] = [] # a network can be comprised of multiple APs
self._active = True # used to not run when not in settings self._active = True # used to not run when not in settings
self._exit = False self._exit = False
@ -132,15 +139,23 @@ class WifiManager:
# State # State
self._connecting_to_ssid: str = "" self._connecting_to_ssid: str = ""
self._ipv4_address: str = ""
self._current_network_metered: MeteredType = MeteredType.UNKNOWN
self._tethering_password: str = ""
self._last_network_update: float = 0.0 self._last_network_update: float = 0.0
self._callback_queue: list[Callable] = [] self._callback_queue: list[Callable] = []
self._tethering_ssid = "weedle"
dongle_id = Params().get("DongleId")
if dongle_id:
self._tethering_ssid += "-" + dongle_id[:4]
# Callbacks # Callbacks
self._need_auth: Callable[[str], None] | None = None self._need_auth: list[Callable[[str], None]] = []
self._activated: Callable[[], None] | None = None self._activated: list[Callable[[], None]] = []
self._forgotten: Callable[[], None] | None = None self._forgotten: list[Callable[[], None]] = []
self._networks_updated: Callable[[list[Network]], None] | None = None self._networks_updated: list[Callable[[list[Network]], None]] = []
self._disconnected: Callable[[], None] | None = None self._disconnected: list[Callable[[], None]] = []
self._lock = threading.Lock() self._lock = threading.Lock()
@ -152,19 +167,37 @@ class WifiManager:
atexit.register(self.stop) atexit.register(self.stop)
def set_callbacks(self, need_auth: Callable[[str], None], def set_callbacks(self, need_auth: Callable[[str], None] | None = None,
activated: Callable[[], None] | None, activated: Callable[[], None] | None = None,
forgotten: Callable[[], None], forgotten: Callable[[], None] | None = None,
networks_updated: Callable[[list[Network]], None], networks_updated: Callable[[list[Network]], None] | None = None,
disconnected: Callable[[], None]): disconnected: Callable[[], None] | None = None):
self._need_auth = need_auth if need_auth is not None:
self._activated = activated self._need_auth.append(need_auth)
self._forgotten = forgotten if activated is not None:
self._networks_updated = networks_updated self._activated.append(activated)
self._disconnected = disconnected if forgotten is not None:
self._forgotten.append(forgotten)
def _enqueue_callback(self, cb: Callable, *args): if networks_updated is not None:
self._callback_queue.append(lambda: cb(*args)) self._networks_updated.append(networks_updated)
if disconnected is not None:
self._disconnected.append(disconnected)
@property
def ipv4_address(self) -> str:
return self._ipv4_address
@property
def current_network_metered(self) -> MeteredType:
return self._current_network_metered
@property
def tethering_password(self) -> str:
return self._tethering_password
def _enqueue_callbacks(self, cbs: list[Callable], *args):
for cb in cbs:
self._callback_queue.append(lambda _cb=cb: _cb(*args))
def process_callbacks(self): def process_callbacks(self):
# Call from UI thread to run any pending callbacks # Call from UI thread to run any pending callbacks
@ -180,10 +213,13 @@ class WifiManager:
self._last_network_update = 0.0 self._last_network_update = 0.0
def _monitor_state(self): def _monitor_state(self):
# TODO: make an initialize function to only wait in one place
device_path = self._wait_for_wifi_device() device_path = self._wait_for_wifi_device()
if device_path is None: if device_path is None:
return return
self._tethering_password = self._get_tethering_password()
rule = MatchRule( rule = MatchRule(
type="signal", type="signal",
interface=NM_DEVICE_IFACE, interface=NM_DEVICE_IFACE,
@ -211,20 +247,18 @@ class WifiManager:
# BAD PASSWORD # BAD PASSWORD
if new_state == NMDeviceState.NEED_AUTH and change_reason == NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT and len(self._connecting_to_ssid): if new_state == NMDeviceState.NEED_AUTH and change_reason == NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT and len(self._connecting_to_ssid):
self.forget_connection(self._connecting_to_ssid, block=True) self.forget_connection(self._connecting_to_ssid, block=True)
if self._need_auth is not None: self._enqueue_callbacks(self._need_auth, self._connecting_to_ssid)
self._enqueue_callback(self._need_auth, self._connecting_to_ssid)
self._connecting_to_ssid = "" self._connecting_to_ssid = ""
elif new_state == NMDeviceState.ACTIVATED: elif new_state == NMDeviceState.ACTIVATED:
if self._activated is not None: if len(self._activated):
self._update_networks() self._update_networks()
self._enqueue_callback(self._activated) self._enqueue_callbacks(self._activated)
self._connecting_to_ssid = "" self._connecting_to_ssid = ""
elif new_state == NMDeviceState.DISCONNECTED and change_reason != NM_DEVICE_STATE_REASON_NEW_ACTIVATION: elif new_state == NMDeviceState.DISCONNECTED and change_reason != NM_DEVICE_STATE_REASON_NEW_ACTIVATION:
self._connecting_to_ssid = "" self._connecting_to_ssid = ""
if self._disconnected is not None: self._enqueue_callbacks(self._forgotten)
self._enqueue_callback(self._disconnected)
def _network_scanner(self): def _network_scanner(self):
self._wait_for_wifi_device() self._wait_for_wifi_device()
@ -285,14 +319,24 @@ class WifiManager:
conns[ssid] = conn_path conns[ssid] = conn_path
return conns return conns
def connect_to_network(self, ssid: str, password: str): def _get_active_connections(self):
return self._router_main.send_and_get_reply(Properties(self._nm).get('ActiveConnections')).body[0][1]
# TODO: use this
def _get_connection_settings(self, conn_path: str) -> dict:
conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE)
reply = self._router_main.send_and_get_reply(new_method_call(conn_addr, 'GetSettings'))
if reply.header.message_type == MessageType.error:
cloudlog.warning(f'Failed to get connection settings: {reply}')
return {}
return dict(reply.body[0])
def connect_to_network(self, ssid: str, password: str, hidden: bool = False):
def worker(): def worker():
# Clear all connections that may already exist to the network we are connecting to # Clear all connections that may already exist to the network we are connecting to
self._connecting_to_ssid = ssid self._connecting_to_ssid = ssid
self.forget_connection(ssid, block=True) self.forget_connection(ssid, block=True)
is_hidden = False
connection = { connection = {
'connection': { 'connection': {
'type': ('s', '802-11-wireless'), 'type': ('s', '802-11-wireless'),
@ -302,7 +346,7 @@ class WifiManager:
}, },
'802-11-wireless': { '802-11-wireless': {
'ssid': ('ay', ssid.encode("utf-8")), 'ssid': ('ay', ssid.encode("utf-8")),
'hidden': ('b', is_hidden), 'hidden': ('b', hidden),
'mode': ('s', 'infrastructure'), 'mode': ('s', 'infrastructure'),
}, },
'ipv4': { 'ipv4': {
@ -332,9 +376,9 @@ class WifiManager:
conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE) conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE)
self._router_main.send_and_get_reply(new_method_call(conn_addr, 'Delete')) self._router_main.send_and_get_reply(new_method_call(conn_addr, 'Delete'))
if self._forgotten is not None: if len(self._forgotten):
self._update_networks() self._update_networks()
self._enqueue_callback(self._forgotten) self._enqueue_callbacks(self._forgotten)
if block: if block:
worker() worker()
@ -358,6 +402,137 @@ class WifiManager:
else: else:
threading.Thread(target=worker, daemon=True).start() threading.Thread(target=worker, daemon=True).start()
def _deactivate_connection(self, ssid: str):
for conn_path in self._get_active_connections():
conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_ACTIVE_CONNECTION_IFACE)
specific_obj_path = self._router_main.send_and_get_reply(Properties(conn_addr).get('SpecificObject')).body[0][1]
if specific_obj_path != "/":
ap_addr = DBusAddress(specific_obj_path, bus_name=NM, interface=NM_ACCESS_POINT_IFACE)
ap_ssid = bytes(self._router_main.send_and_get_reply(Properties(ap_addr).get('Ssid')).body[0][1]).decode("utf-8", "replace")
if ap_ssid == ssid:
self._router_main.send_and_get_reply(new_method_call(self._nm, 'DeactivateConnection', 'o', (conn_path,)))
return
def is_tethering_active(self) -> bool:
for network in self._networks:
if network.is_connected:
return bool(network.ssid == self._tethering_ssid)
return False
def set_tethering_password(self, password: str):
def worker():
conn_path = self._get_connections().get(self._tethering_ssid, None)
if conn_path is None:
cloudlog.warning('No tethering connection found')
return
settings = self._get_connection_settings(conn_path)
if len(settings) == 0:
cloudlog.warning(f'Failed to get tethering settings for {conn_path}')
return
settings['802-11-wireless-security']['psk'] = ('s', password)
conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE)
reply = self._router_main.send_and_get_reply(new_method_call(conn_addr, 'Update', 'a{sa{sv}}', (settings,)))
if reply.header.message_type == MessageType.error:
cloudlog.warning(f'Failed to update tethering settings: {reply}')
return
self._tethering_password = password
if self.is_tethering_active():
self.activate_connection(self._tethering_ssid, block=True)
threading.Thread(target=worker, daemon=True).start()
def _get_tethering_password(self) -> str:
conn_path = self._get_connections().get(self._tethering_ssid, None)
if conn_path is None:
cloudlog.warning('No tethering connection found')
return ''
reply = self._router_main.send_and_get_reply(new_method_call(
DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE),
'GetSecrets', 's', ('802-11-wireless-security',)
))
if reply.header.message_type == MessageType.error:
cloudlog.warning(f'Failed to get tethering password: {reply}')
return ''
secrets = reply.body[0]
if '802-11-wireless-security' not in secrets:
return ''
return str(secrets['802-11-wireless-security'].get('psk', ('s', ''))[1])
def set_tethering_active(self, active: bool):
def worker():
if active:
self.activate_connection(self._tethering_ssid, block=True)
else:
self._deactivate_connection(self._tethering_ssid)
threading.Thread(target=worker, daemon=True).start()
def _update_current_network_metered(self) -> None:
if self._wifi_device is None:
cloudlog.warning("No WiFi device found")
return
self._current_network_metered = MeteredType.UNKNOWN
for active_conn in self._get_active_connections():
conn_addr = DBusAddress(active_conn, bus_name=NM, interface=NM_ACTIVE_CONNECTION_IFACE)
conn_type = self._router_main.send_and_get_reply(Properties(conn_addr).get('Type')).body[0][1]
if conn_type == '802-11-wireless':
conn_path = self._router_main.send_and_get_reply(Properties(conn_addr).get('Connection')).body[0][1]
if conn_path == "/":
continue
settings = self._get_connection_settings(conn_path)
if len(settings) == 0:
cloudlog.warning(f'Failed to get connection settings for {conn_path}')
continue
metered_prop = settings['connection'].get('metered', ('i', 0))[1]
if metered_prop == MeteredType.YES:
self._current_network_metered = MeteredType.YES
elif metered_prop == MeteredType.NO:
self._current_network_metered = MeteredType.NO
print('current_network_metered', self._current_network_metered)
return
def set_current_network_metered(self, metered: MeteredType):
def worker():
for active_conn in self._get_active_connections():
conn_addr = DBusAddress(active_conn, bus_name=NM, interface=NM_ACTIVE_CONNECTION_IFACE)
conn_type = self._router_main.send_and_get_reply(Properties(conn_addr).get('Type')).body[0][1]
if conn_type == '802-11-wireless' and not self.is_tethering_active():
conn_path = self._router_main.send_and_get_reply(Properties(conn_addr).get('Connection')).body[0][1]
if conn_path == "/":
continue
settings = self._get_connection_settings(conn_path)
if len(settings) == 0:
cloudlog.warning(f'Failed to get connection settings for {conn_path}')
return
settings['connection']['metered'] = ('i', int(metered))
conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE)
reply = self._router_main.send_and_get_reply(new_method_call(conn_addr, 'Update', 'a{sa{sv}}', (settings,)))
if reply.header.message_type == MessageType.error:
cloudlog.warning(f'Failed to update tethering settings: {reply}')
return
threading.Thread(target=worker, daemon=True).start()
def _request_scan(self): def _request_scan(self):
if self._wifi_device is None: if self._wifi_device is None:
cloudlog.warning("No WiFi device found") cloudlog.warning("No WiFi device found")
@ -409,8 +584,32 @@ class WifiManager:
networks.sort(key=lambda n: (-n.is_connected, -n.strength, n.ssid.lower())) networks.sort(key=lambda n: (-n.is_connected, -n.strength, n.ssid.lower()))
self._networks = networks self._networks = networks
if self._networks_updated is not None: self._update_ipv4_address()
self._enqueue_callback(self._networks_updated, self._networks) self._update_current_network_metered()
self._enqueue_callbacks(self._networks_updated, self._networks)
def _update_ipv4_address(self):
if self._wifi_device is None:
cloudlog.warning("No WiFi device found")
return
self._ipv4_address = ""
for conn_path in self._get_active_connections():
conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_ACTIVE_CONNECTION_IFACE)
conn_type = self._router_main.send_and_get_reply(Properties(conn_addr).get('Type')).body[0][1]
if conn_type == '802-11-wireless':
ip4config_path = self._router_main.send_and_get_reply(Properties(conn_addr).get('Ip4Config')).body[0][1]
if ip4config_path != "/":
ip4config_addr = DBusAddress(ip4config_path, bus_name=NM, interface=NM_IP4_CONFIG_IFACE)
address_data = self._router_main.send_and_get_reply(Properties(ip4config_addr).get('AddressData')).body[0][1]
for entry in address_data:
if 'address' in entry:
self._ipv4_address = entry['address'][1]
return
def __del__(self): def __del__(self):
self.stop() self.stop()

@ -14,6 +14,7 @@ class ButtonStyle(IntEnum):
PRIMARY = 1 # For main actions PRIMARY = 1 # For main actions
DANGER = 2 # For critical actions, like reboot or delete DANGER = 2 # For critical actions, like reboot or delete
TRANSPARENT = 3 # For buttons with transparent background and border TRANSPARENT = 3 # For buttons with transparent background and border
TRANSPARENT_WHITE = 3 # For buttons with transparent background and border
ACTION = 4 ACTION = 4
LIST_ACTION = 5 # For list items with action buttons LIST_ACTION = 5 # For list items with action buttons
NO_EFFECT = 6 NO_EFFECT = 6
@ -23,8 +24,6 @@ class ButtonStyle(IntEnum):
ICON_PADDING = 15 ICON_PADDING = 15
DEFAULT_BUTTON_FONT_SIZE = 60 DEFAULT_BUTTON_FONT_SIZE = 60
BUTTON_DISABLED_TEXT_COLOR = rl.Color(228, 228, 228, 51)
BUTTON_DISABLED_BACKGROUND_COLOR = rl.Color(51, 51, 51, 255)
ACTION_BUTTON_FONT_SIZE = 48 ACTION_BUTTON_FONT_SIZE = 48
BUTTON_TEXT_COLOR = { BUTTON_TEXT_COLOR = {
@ -32,6 +31,7 @@ BUTTON_TEXT_COLOR = {
ButtonStyle.PRIMARY: rl.Color(228, 228, 228, 255), ButtonStyle.PRIMARY: rl.Color(228, 228, 228, 255),
ButtonStyle.DANGER: rl.Color(228, 228, 228, 255), ButtonStyle.DANGER: rl.Color(228, 228, 228, 255),
ButtonStyle.TRANSPARENT: rl.BLACK, ButtonStyle.TRANSPARENT: rl.BLACK,
ButtonStyle.TRANSPARENT_WHITE: rl.WHITE,
ButtonStyle.ACTION: rl.BLACK, ButtonStyle.ACTION: rl.BLACK,
ButtonStyle.LIST_ACTION: rl.Color(228, 228, 228, 255), ButtonStyle.LIST_ACTION: rl.Color(228, 228, 228, 255),
ButtonStyle.NO_EFFECT: rl.Color(228, 228, 228, 255), ButtonStyle.NO_EFFECT: rl.Color(228, 228, 228, 255),
@ -39,11 +39,16 @@ BUTTON_TEXT_COLOR = {
ButtonStyle.FORGET_WIFI: rl.Color(51, 51, 51, 255), ButtonStyle.FORGET_WIFI: rl.Color(51, 51, 51, 255),
} }
BUTTON_DISABLED_TEXT_COLORS = {
ButtonStyle.TRANSPARENT_WHITE: rl.WHITE,
}
BUTTON_BACKGROUND_COLORS = { BUTTON_BACKGROUND_COLORS = {
ButtonStyle.NORMAL: rl.Color(51, 51, 51, 255), ButtonStyle.NORMAL: rl.Color(51, 51, 51, 255),
ButtonStyle.PRIMARY: rl.Color(70, 91, 234, 255), ButtonStyle.PRIMARY: rl.Color(70, 91, 234, 255),
ButtonStyle.DANGER: rl.Color(255, 36, 36, 255), ButtonStyle.DANGER: rl.Color(255, 36, 36, 255),
ButtonStyle.TRANSPARENT: rl.BLACK, ButtonStyle.TRANSPARENT: rl.BLACK,
ButtonStyle.TRANSPARENT_WHITE: rl.BLANK,
ButtonStyle.ACTION: rl.Color(189, 189, 189, 255), ButtonStyle.ACTION: rl.Color(189, 189, 189, 255),
ButtonStyle.LIST_ACTION: rl.Color(57, 57, 57, 255), ButtonStyle.LIST_ACTION: rl.Color(57, 57, 57, 255),
ButtonStyle.NO_EFFECT: rl.Color(51, 51, 51, 255), ButtonStyle.NO_EFFECT: rl.Color(51, 51, 51, 255),
@ -56,6 +61,7 @@ BUTTON_PRESSED_BACKGROUND_COLORS = {
ButtonStyle.PRIMARY: rl.Color(48, 73, 244, 255), ButtonStyle.PRIMARY: rl.Color(48, 73, 244, 255),
ButtonStyle.DANGER: rl.Color(255, 36, 36, 255), ButtonStyle.DANGER: rl.Color(255, 36, 36, 255),
ButtonStyle.TRANSPARENT: rl.BLACK, ButtonStyle.TRANSPARENT: rl.BLACK,
ButtonStyle.TRANSPARENT_WHITE: rl.BLANK,
ButtonStyle.ACTION: rl.Color(130, 130, 130, 255), ButtonStyle.ACTION: rl.Color(130, 130, 130, 255),
ButtonStyle.LIST_ACTION: rl.Color(74, 74, 74, 74), ButtonStyle.LIST_ACTION: rl.Color(74, 74, 74, 74),
ButtonStyle.NO_EFFECT: rl.Color(51, 51, 51, 255), ButtonStyle.NO_EFFECT: rl.Color(51, 51, 51, 255),
@ -63,6 +69,10 @@ BUTTON_PRESSED_BACKGROUND_COLORS = {
ButtonStyle.FORGET_WIFI: rl.Color(130, 130, 130, 255), ButtonStyle.FORGET_WIFI: rl.Color(130, 130, 130, 255),
} }
BUTTON_DISABLED_BACKGROUND_COLORS = {
ButtonStyle.TRANSPARENT_WHITE: rl.BLANK,
}
_pressed_buttons: set[str] = set() # Track mouse press state globally _pressed_buttons: set[str] = set() # Track mouse press state globally
@ -156,7 +166,7 @@ def gui_button(
# Draw the button text if any # Draw the button text if any
if text: if text:
color = BUTTON_TEXT_COLOR[button_style] if is_enabled else BUTTON_DISABLED_TEXT_COLOR color = BUTTON_TEXT_COLOR[button_style] if is_enabled else BUTTON_DISABLED_TEXT_COLORS.get(button_style, rl.Color(228, 228, 228, 51))
rl.draw_text_ex(font, text, text_pos, font_size, 0, color) rl.draw_text_ex(font, text, text_pos, font_size, 0, color)
return result return result
@ -198,8 +208,8 @@ class Button(Widget):
else: else:
self._background_color = BUTTON_BACKGROUND_COLORS[self._button_style] self._background_color = BUTTON_BACKGROUND_COLORS[self._button_style]
elif self._button_style != ButtonStyle.NO_EFFECT: elif self._button_style != ButtonStyle.NO_EFFECT:
self._background_color = BUTTON_DISABLED_BACKGROUND_COLOR self._background_color = BUTTON_DISABLED_BACKGROUND_COLORS.get(self._button_style, rl.Color(51, 51, 51, 255))
self._label.set_text_color(BUTTON_DISABLED_TEXT_COLOR) self._label.set_text_color(BUTTON_DISABLED_TEXT_COLORS.get(self._button_style, rl.Color(228, 228, 228, 51)))
def _render(self, _): def _render(self, _):
roundness = self._border_radius / (min(self._rect.width, self._rect.height) / 2) roundness = self._border_radius / (min(self._rect.width, self._rect.height) / 2)

@ -104,6 +104,9 @@ class Keyboard(Widget):
self._all_keys[CAPS_LOCK_KEY] = Button("", partial(self._key_callback, CAPS_LOCK_KEY), icon=self._key_icons[CAPS_LOCK_KEY], self._all_keys[CAPS_LOCK_KEY] = Button("", partial(self._key_callback, CAPS_LOCK_KEY), icon=self._key_icons[CAPS_LOCK_KEY],
button_style=ButtonStyle.KEYBOARD, multi_touch=True) button_style=ButtonStyle.KEYBOARD, multi_touch=True)
def set_text(self, text: str):
self._input_box.text = text
@property @property
def text(self): def text(self):
return self._input_box.text return self._input_box.text
@ -243,7 +246,9 @@ class Keyboard(Widget):
if not self._caps_lock and self._layout_name == "uppercase": if not self._caps_lock and self._layout_name == "uppercase":
self._layout_name = "lowercase" self._layout_name = "lowercase"
def reset(self): def reset(self, min_text_size: int | None = None):
if min_text_size is not None:
self._min_text_size = min_text_size
self._render_return_status = -1 self._render_return_status = -1
self.clear() self.clear()

@ -41,6 +41,9 @@ class ItemAction(Widget, ABC):
self.set_rect(rl.Rectangle(0, 0, width, 0)) self.set_rect(rl.Rectangle(0, 0, width, 0))
self._enabled_source = enabled self._enabled_source = enabled
def set_enabled(self, enabled: bool | Callable[[], bool]):
self._enabled_source = enabled
@property @property
def enabled(self): def enabled(self):
return _resolve_value(self._enabled_source, False) return _resolve_value(self._enabled_source, False)
@ -58,8 +61,9 @@ class ToggleAction(ItemAction):
def _render(self, rect: rl.Rectangle) -> bool: def _render(self, rect: rl.Rectangle) -> bool:
self.toggle.set_enabled(self.enabled) self.toggle.set_enabled(self.enabled)
self.toggle.render(rl.Rectangle(rect.x, rect.y + (rect.height - TOGGLE_HEIGHT) / 2, self._rect.width, TOGGLE_HEIGHT)) clicked = self.toggle.render(rl.Rectangle(rect.x, rect.y + (rect.height - TOGGLE_HEIGHT) / 2, self._rect.width, TOGGLE_HEIGHT))
return False self.state = self.toggle.get_state()
return bool(clicked)
def set_state(self, state: bool): def set_state(self, state: bool):
self.state = state self.state = state
@ -86,7 +90,7 @@ class ButtonAction(ItemAction):
border_radius=BUTTON_BORDER_RADIUS, border_radius=BUTTON_BORDER_RADIUS,
click_callback=pressed, click_callback=pressed,
) )
self._button.set_enabled(_resolve_value(enabled)) self.set_enabled(enabled)
def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None: def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None:
super().set_touch_valid_callback(touch_callback) super().set_touch_valid_callback(touch_callback)
@ -98,6 +102,7 @@ class ButtonAction(ItemAction):
def _render(self, rect: rl.Rectangle) -> bool: def _render(self, rect: rl.Rectangle) -> bool:
self._button.set_text(self.text) self._button.set_text(self.text)
self._button.set_enabled(_resolve_value(self.enabled))
button_rect = rl.Rectangle(rect.x, rect.y + (rect.height - BUTTON_HEIGHT) / 2, BUTTON_WIDTH, BUTTON_HEIGHT) button_rect = rl.Rectangle(rect.x, rect.y + (rect.height - BUTTON_HEIGHT) / 2, BUTTON_WIDTH, BUTTON_HEIGHT)
self._button.render(button_rect) self._button.render(button_rect)
@ -121,6 +126,10 @@ class TextAction(ItemAction):
def text(self): def text(self):
return _resolve_value(self._text_source, "Error") return _resolve_value(self._text_source, "Error")
def _update_state(self):
text_width = measure_text_cached(self._font, self.text, ITEM_TEXT_FONT_SIZE).x
self._rect.width = int(text_width + TEXT_PADDING)
def _render(self, rect: rl.Rectangle) -> bool: def _render(self, rect: rl.Rectangle) -> bool:
current_text = self.text current_text = self.text
text_size = measure_text_cached(self._font, current_text, ITEM_TEXT_FONT_SIZE) text_size = measure_text_cached(self._font, current_text, ITEM_TEXT_FONT_SIZE)
@ -183,7 +192,7 @@ class MultipleButtonAction(ItemAction):
# Check button state # Check button state
mouse_pos = rl.get_mouse_position() mouse_pos = rl.get_mouse_position()
is_hovered = rl.check_collision_point_rec(mouse_pos, button_rect) is_hovered = rl.check_collision_point_rec(mouse_pos, button_rect) and self.enabled
is_pressed = is_hovered and rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and self.is_pressed is_pressed = is_hovered and rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and self.is_pressed
is_selected = i == self.selected_button is_selected = i == self.selected_button
@ -195,6 +204,9 @@ class MultipleButtonAction(ItemAction):
else: else:
bg_color = rl.Color(57, 57, 57, 255) # Gray bg_color = rl.Color(57, 57, 57, 255) # Gray
if not self.enabled:
bg_color = rl.Color(bg_color.r, bg_color.g, bg_color.b, 150) # Dim
# Draw button # Draw button
rl.draw_rectangle_rounded(button_rect, 1.0, 20, bg_color) rl.draw_rectangle_rounded(button_rect, 1.0, 20, bg_color)
@ -202,7 +214,8 @@ class MultipleButtonAction(ItemAction):
text_size = measure_text_cached(self._font, text, 40) text_size = measure_text_cached(self._font, text, 40)
text_x = button_x + (self.button_width - text_size.x) / 2 text_x = button_x + (self.button_width - text_size.x) / 2
text_y = button_y + (BUTTON_HEIGHT - text_size.y) / 2 text_y = button_y + (BUTTON_HEIGHT - text_size.y) / 2
rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), 40, 0, rl.Color(228, 228, 228, 255)) text_color = rl.Color(228, 228, 228, 255) if self.enabled else rl.Color(150, 150, 150, 255)
rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), 40, 0, text_color)
# Handle click # Handle click
if is_hovered and rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) and self.is_pressed: if is_hovered and rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) and self.is_pressed:

@ -3,14 +3,17 @@ from functools import partial
from typing import cast from typing import cast
import pyray as rl import pyray as rl
from openpilot.system.ui.lib.application import gui_app from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.system.ui.lib.application import gui_app, DEFAULT_FPS
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
from openpilot.system.ui.lib.wifi_manager import WifiManager, SecurityType, Network from openpilot.system.ui.lib.wifi_manager import WifiManager, SecurityType, Network, MeteredType
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import ButtonStyle, Button from openpilot.system.ui.widgets.button import ButtonStyle, Button
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
from openpilot.system.ui.widgets.keyboard import Keyboard from openpilot.system.ui.widgets.keyboard import Keyboard
from openpilot.system.ui.widgets.label import TextAlignment, gui_label from openpilot.system.ui.widgets.label import TextAlignment, gui_label
from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.system.ui.widgets.list_view import text_item, button_item, ListItem, ToggleAction, MultipleButtonAction, ButtonAction
NM_DEVICE_STATE_NEED_AUTH = 60 NM_DEVICE_STATE_NEED_AUTH = 60
MIN_PASSWORD_LENGTH = 8 MIN_PASSWORD_LENGTH = 8
@ -26,6 +29,11 @@ STRENGTH_ICONS = [
] ]
class PanelType(IntEnum):
WIFI = 0
ADVANCED = 1
class UIState(IntEnum): class UIState(IntEnum):
IDLE = 0 IDLE = 0
CONNECTING = 1 CONNECTING = 1
@ -34,10 +42,179 @@ class UIState(IntEnum):
FORGETTING = 4 FORGETTING = 4
class NavButton(Widget):
def __init__(self, text: str):
super().__init__()
self.text = text
self.set_rect(rl.Rectangle(0, 0, 400, 100))
self._x_pos_filter = FirstOrderFilter(0.0, 0.05, 1 / DEFAULT_FPS, initialized=False)
self._y_pos_filter = FirstOrderFilter(0.0, 0.05, 1 / DEFAULT_FPS, initialized=False)
def set_position(self, x: float, y: float) -> None:
x = self._x_pos_filter.update(x)
y = self._y_pos_filter.update(y)
changed = (self._rect.x != x or self._rect.y != y)
self._rect.x, self._rect.y = x, y
if changed:
self._update_layout_rects()
def _render(self, _):
color = rl.Color(74, 74, 74, 255) if self.is_pressed else rl.Color(57, 57, 57, 255)
rl.draw_rectangle_rounded(self._rect, 0.6, 10, color)
gui_label(self.rect, self.text, font_size=60, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
class NetworkUI(Widget):
def __init__(self, wifi_manager: WifiManager):
super().__init__()
self._wifi_manager = wifi_manager
self._current_panel: PanelType = PanelType.WIFI
self._wifi_panel = WifiManagerUI(wifi_manager)
self._advanced_panel = AdvancedNetworkSettings(wifi_manager)
self._nav_button = NavButton("Advanced")
self._nav_button.set_click_callback(self._cycle_panel)
def _update_state(self):
self._wifi_manager.process_callbacks()
def show_event(self):
self._set_current_panel(PanelType.WIFI)
self._wifi_panel.show_event()
def hide_event(self):
self._wifi_panel.hide_event()
def _cycle_panel(self):
if self._current_panel == PanelType.WIFI:
self._set_current_panel(PanelType.ADVANCED)
else:
self._set_current_panel(PanelType.WIFI)
def _render(self, _):
# subtract button
content_rect = rl.Rectangle(self._rect.x, self._rect.y + self._nav_button.rect.height + 20,
self._rect.width, self._rect.height - self._nav_button.rect.height - 20)
if self._current_panel == PanelType.WIFI:
self._nav_button.text = "Advanced"
self._nav_button.set_position(self._rect.x + self._rect.width - self._nav_button.rect.width, self._rect.y + 10)
self._wifi_panel.render(content_rect)
else:
self._nav_button.text = "Back"
self._nav_button.set_position(self._rect.x, self._rect.y + 10)
self._advanced_panel.render(content_rect)
self._nav_button.render()
def _set_current_panel(self, panel: PanelType):
self._current_panel = panel
class AdvancedNetworkSettings(Widget):
def __init__(self, wifi_manager: WifiManager):
super().__init__()
self._wifi_manager = wifi_manager
self._wifi_manager.set_callbacks(networks_updated=self._on_network_updated)
self._keyboard = Keyboard(max_text_size=MAX_PASSWORD_LENGTH, min_text_size=MIN_PASSWORD_LENGTH, show_password_toggle=True)
# Tethering
self._tethering_action = ToggleAction(initial_state=False)
self._tethering_btn = ListItem(title="Enable Tethering", action_item=self._tethering_action, callback=self._toggle_tethering)
# Tethering Password
self._tethering_password_action = ButtonAction(text="EDIT")
self._tethering_password_btn = ListItem(title="Tethering Password", action_item=self._tethering_password_action, callback=self._edit_tethering_password)
# TODO: Roaming toggle, edit APN settings, and cellular metered toggle
# Metered
self._wifi_metered_action = MultipleButtonAction(["default", "metered", "unmetered"], 255, 0, callback=self._toggle_wifi_metered)
self._wifi_metered_btn = ListItem(title="Wi-Fi Network Metered", description="Prevent large data uploads when on a metered Wi-Fi connection",
action_item=self._wifi_metered_action)
items: list[Widget] = [
self._tethering_btn,
self._tethering_password_btn,
text_item("IP Address", lambda: self._wifi_manager.ipv4_address),
self._wifi_metered_btn,
button_item("Hidden Network", "CONNECT", callback=self._connect_to_hidden_network),
]
self._scroller = Scroller(items, line_separator=True, spacing=0)
def _on_network_updated(self, networks: list[Network]):
self._tethering_action.set_enabled(True)
self._tethering_action.set_state(self._wifi_manager.is_tethering_active())
self._tethering_password_action.set_enabled(True)
if self._wifi_manager.is_tethering_active() or self._wifi_manager.ipv4_address == "":
self._wifi_metered_action.set_enabled(False)
self._wifi_metered_action.selected_button = 0
elif self._wifi_manager.ipv4_address != "":
metered = self._wifi_manager.current_network_metered
self._wifi_metered_action.set_enabled(True)
self._wifi_metered_action.selected_button = int(metered) if metered in (MeteredType.UNKNOWN, MeteredType.YES, MeteredType.NO) else 0
def _toggle_tethering(self):
checked = self._tethering_action.state
self._tethering_action.set_enabled(False)
if checked:
self._wifi_metered_action.set_enabled(False)
self._wifi_manager.set_tethering_active(checked)
def _toggle_wifi_metered(self, metered):
metered_type = {0: MeteredType.UNKNOWN, 1: MeteredType.YES, 2: MeteredType.NO}.get(metered, MeteredType.UNKNOWN)
self._wifi_metered_action.set_enabled(False)
self._wifi_manager.set_current_network_metered(metered_type)
def _connect_to_hidden_network(self):
def connect_hidden(result):
if result != 1:
return
ssid = self._keyboard.text
if not ssid:
return
def enter_password(result):
password = self._keyboard.text
if password == "":
# connect without password
self._wifi_manager.connect_to_network(ssid, "", hidden=True)
return
self._wifi_manager.connect_to_network(ssid, password, hidden=True)
self._keyboard.reset(min_text_size=0)
self._keyboard.set_title("Enter password", f"for \"{ssid}\"")
gui_app.set_modal_overlay(self._keyboard, enter_password)
self._keyboard.reset(min_text_size=1)
self._keyboard.set_title("Enter SSID", "")
gui_app.set_modal_overlay(self._keyboard, connect_hidden)
def _edit_tethering_password(self):
def update_password(result):
if result != 1:
return
password = self._keyboard.text
self._wifi_manager.set_tethering_password(password)
self._tethering_password_action.set_enabled(False)
self._keyboard.reset(min_text_size=MIN_PASSWORD_LENGTH)
self._keyboard.set_title("Enter new tethering password", "")
self._keyboard.set_text(self._wifi_manager.tethering_password)
gui_app.set_modal_overlay(self._keyboard, update_password)
def _render(self, _):
self._scroller.render(self._rect)
class WifiManagerUI(Widget): class WifiManagerUI(Widget):
def __init__(self, wifi_manager: WifiManager): def __init__(self, wifi_manager: WifiManager):
super().__init__() super().__init__()
self.wifi_manager = wifi_manager self._wifi_manager = wifi_manager
self.state: UIState = UIState.IDLE self.state: UIState = UIState.IDLE
self._state_network: Network | None = None # for CONNECTING / NEEDS_AUTH / SHOW_FORGET_CONFIRM / FORGETTING self._state_network: Network | None = None # for CONNECTING / NEEDS_AUTH / SHOW_FORGET_CONFIRM / FORGETTING
self._password_retry: bool = False # for NEEDS_AUTH self._password_retry: bool = False # for NEEDS_AUTH
@ -51,7 +228,7 @@ class WifiManagerUI(Widget):
self._forget_networks_buttons: dict[str, Button] = {} self._forget_networks_buttons: dict[str, Button] = {}
self._confirm_dialog = ConfirmDialog("", "Forget", "Cancel") self._confirm_dialog = ConfirmDialog("", "Forget", "Cancel")
self.wifi_manager.set_callbacks(need_auth=self._on_need_auth, self._wifi_manager.set_callbacks(need_auth=self._on_need_auth,
activated=self._on_activated, activated=self._on_activated,
forgotten=self._on_forgotten, forgotten=self._on_forgotten,
networks_updated=self._on_network_updated, networks_updated=self._on_network_updated,
@ -59,25 +236,23 @@ class WifiManagerUI(Widget):
def show_event(self): def show_event(self):
# start/stop scanning when widget is visible # start/stop scanning when widget is visible
self.wifi_manager.set_active(True) self._wifi_manager.set_active(True)
def hide_event(self): def hide_event(self):
self.wifi_manager.set_active(False) self._wifi_manager.set_active(False)
def _load_icons(self): def _load_icons(self):
for icon in STRENGTH_ICONS + ["icons/checkmark.png", "icons/circled_slash.png", "icons/lock_closed.png"]: for icon in STRENGTH_ICONS + ["icons/checkmark.png", "icons/circled_slash.png", "icons/lock_closed.png"]:
gui_app.texture(icon, ICON_SIZE, ICON_SIZE) gui_app.texture(icon, ICON_SIZE, ICON_SIZE)
def _render(self, rect: rl.Rectangle): def _render(self, rect: rl.Rectangle):
self.wifi_manager.process_callbacks()
if not self._networks: if not self._networks:
gui_label(rect, "Scanning Wi-Fi networks...", 72, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) gui_label(rect, "Scanning Wi-Fi networks...", 72, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
return return
if self.state == UIState.NEEDS_AUTH and self._state_network: if self.state == UIState.NEEDS_AUTH and self._state_network:
self.keyboard.set_title("Wrong password" if self._password_retry else "Enter password", f"for {self._state_network.ssid}") self.keyboard.set_title("Wrong password" if self._password_retry else "Enter password", f"for {self._state_network.ssid}")
self.keyboard.reset() self.keyboard.reset(min_text_size=MIN_PASSWORD_LENGTH)
gui_app.set_modal_overlay(self.keyboard, lambda result: self._on_password_entered(cast(Network, self._state_network), result)) gui_app.set_modal_overlay(self.keyboard, lambda result: self._on_password_entered(cast(Network, self._state_network), result))
elif self.state == UIState.SHOW_FORGET_CONFIRM and self._state_network: elif self.state == UIState.SHOW_FORGET_CONFIRM and self._state_network:
self._confirm_dialog.set_text(f'Forget Wi-Fi Network "{self._state_network.ssid}"?') self._confirm_dialog.set_text(f'Forget Wi-Fi Network "{self._state_network.ssid}"?')
@ -200,20 +375,20 @@ class WifiManagerUI(Widget):
self.state = UIState.CONNECTING self.state = UIState.CONNECTING
self._state_network = network self._state_network = network
if network.is_saved and not password: if network.is_saved and not password:
self.wifi_manager.activate_connection(network.ssid) self._wifi_manager.activate_connection(network.ssid)
else: else:
self.wifi_manager.connect_to_network(network.ssid, password) self._wifi_manager.connect_to_network(network.ssid, password)
def forget_network(self, network: Network): def forget_network(self, network: Network):
self.state = UIState.FORGETTING self.state = UIState.FORGETTING
self._state_network = network self._state_network = network
self.wifi_manager.forget_connection(network.ssid) self._wifi_manager.forget_connection(network.ssid)
def _on_network_updated(self, networks: list[Network]): def _on_network_updated(self, networks: list[Network]):
self._networks = networks self._networks = networks
for n in self._networks: for n in self._networks:
self._networks_buttons[n.ssid] = Button(n.ssid, partial(self._networks_buttons_callback, n), font_size=55, text_alignment=TextAlignment.LEFT, self._networks_buttons[n.ssid] = Button(n.ssid, partial(self._networks_buttons_callback, n), font_size=55, text_alignment=TextAlignment.LEFT,
button_style=ButtonStyle.NO_EFFECT) button_style=ButtonStyle.TRANSPARENT_WHITE)
self._forget_networks_buttons[n.ssid] = Button("Forget", partial(self._forget_networks_buttons_callback, n), button_style=ButtonStyle.FORGET_WIFI, self._forget_networks_buttons[n.ssid] = Button("Forget", partial(self._forget_networks_buttons_callback, n), button_style=ButtonStyle.FORGET_WIFI,
font_size=45) font_size=45)

@ -20,6 +20,7 @@ class Toggle(Widget):
self._enabled = True self._enabled = True
self._progress = 1.0 if initial_state else 0.0 self._progress = 1.0 if initial_state else 0.0
self._target = self._progress self._target = self._progress
self._clicked = False
def set_rect(self, rect: rl.Rectangle): def set_rect(self, rect: rl.Rectangle):
self._rect = rl.Rectangle(rect.x, rect.y, WIDTH, HEIGHT) self._rect = rl.Rectangle(rect.x, rect.y, WIDTH, HEIGHT)
@ -28,6 +29,7 @@ class Toggle(Widget):
if not self._enabled: if not self._enabled:
return return
self._clicked = True
self._state = not self._state self._state = not self._state
self._target = 1.0 if self._state else 0.0 self._target = 1.0 if self._state else 0.0
@ -66,5 +68,10 @@ class Toggle(Widget):
knob_y = self._rect.y + HEIGHT / 2 knob_y = self._rect.y + HEIGHT / 2
rl.draw_circle(int(knob_x), int(knob_y), HEIGHT / 2, knob_color) rl.draw_circle(int(knob_x), int(knob_y), HEIGHT / 2, knob_color)
# TODO: use click callback
clicked = self._clicked
self._clicked = False
return clicked
def _blend_color(self, c1, c2, t): def _blend_color(self, c1, c2, t):
return rl.Color(int(c1.r + (c2.r - c1.r) * t), int(c1.g + (c2.g - c1.g) * t), int(c1.b + (c2.b - c1.b) * t), 255) return rl.Color(int(c1.r + (c2.r - c1.r) * t), int(c1.g + (c2.g - c1.g) * t), int(c1.b + (c2.b - c1.b) * t), 255)

Loading…
Cancel
Save