From 179a8ec007e66f11380335451fd40787a4cd9e6c Mon Sep 17 00:00:00 2001 From: deanlee Date: Sat, 8 Mar 2025 03:07:55 +0800 Subject: [PATCH 01/16] python wifi manager --- system/ui/lib/keyboard.py | 104 ++++++++++ system/ui/lib/wifi_manager.py | 376 ++++++++++++++++++++++++++++++++++ system/ui/network.py | 158 ++++++++++++++ 3 files changed, 638 insertions(+) create mode 100644 system/ui/lib/keyboard.py create mode 100644 system/ui/lib/wifi_manager.py create mode 100644 system/ui/network.py diff --git a/system/ui/lib/keyboard.py b/system/ui/lib/keyboard.py new file mode 100644 index 0000000000..81e0d95754 --- /dev/null +++ b/system/ui/lib/keyboard.py @@ -0,0 +1,104 @@ +import pyray as rl +from openpilot.system.ui.lib.button import gui_button +from openpilot.system.ui.lib.label import gui_label + +# Constants for special keys +BACKSPACE_KEY = "<-" +ENTER_KEY = "Enter" +SPACE_KEY = " " +SHIFT_KEY = "↑" +SHIFT_DOWN_KEY = "↓" +NUMERIC_KEY = "123" +SYMBOL_KEY = "#+=" +ABC_KEY = "ABC" + +# Define keyboard layouts as a dictionary for easier access +keyboard_layouts = { + "lowercase": [ + ["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"], + ["a", "s", "d", "f", "g", "h", "j", "k", "l"], + [SHIFT_KEY, "z", "x", "c", "v", "b", "n", "m", BACKSPACE_KEY], + [NUMERIC_KEY, "/", "-", SPACE_KEY, ".", ENTER_KEY], + ], + "uppercase": [ + ["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"], + ["A", "S", "D", "F", "G", "H", "J", "K", "L"], + [SHIFT_DOWN_KEY, "Z", "X", "C", "V", "B", "N", "M", BACKSPACE_KEY], + [NUMERIC_KEY, "/", "-", SPACE_KEY, ".", ENTER_KEY], + ], + "numbers": [ + ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"], + ["-", "/", ":", ";", "(", ")", "$", "&", "@", "\""], + [SYMBOL_KEY, ".", ",", "?", "!", "`", BACKSPACE_KEY], + [ABC_KEY, SPACE_KEY, ".", ENTER_KEY], + ], + "specials": [ + ["[", "]", "{", "}", "#", "%", "^", "*", "+", "="], + ["_", "\\", "|", "~", "<", ">", "€", "£", "¥", "•"], + [NUMERIC_KEY, ".", ",", "?", "!", "'", BACKSPACE_KEY], + [ABC_KEY, SPACE_KEY, ".", ENTER_KEY], + ], +} + + +class Keyboard: + def __init__(self, max_text_size: int = 255): + self._layout = keyboard_layouts["lowercase"] + self._input_text = "" + self._max_text_size = max_text_size + + @property + def text(self) -> str: + return self._input_text + + def clear(self): + self._input_text = "" + + def render(self, rect, title, sub_title): + gui_label((rect.x, rect.y, rect.width, 95), title, 90) + gui_label((rect.x, rect.y + 95, rect.width, 60), sub_title, 55, rl.GRAY) + if gui_button(rl.Rectangle(rect.x + rect.width - 300, rect.y, 300, 100), "Cancel"): + return -1 + + # Text box for input + rl.gui_text_box(rl.Rectangle(rect.x, rect.y + 160, rect.width, 100), self._input_text, self._max_text_size, True) + + h_space, v_space = 15, 15 + row_y_start = rect.y + 300 # Starting Y position for the first row + key_height = (rect.height - 300 - 3 * v_space) / 4 + key_max_width = (rect.width - (len(self._layout[2]) - 1) * h_space) / len(self._layout[2]) + + # Iterate over the rows of keys in the current layout + for row, keys in enumerate(self._layout): + key_width = min((rect.width - (180 if row == 1 else 0) - h_space * (len(keys) - 1)) / len(keys), key_max_width) + start_x = rect.x + (90 if row == 1 else 0) + + for i, key in enumerate(keys): + if i > 0: + start_x += h_space + + new_width = (key_width * 3 + h_space * 2) if key == SPACE_KEY else (key_width * 2 + h_space if key == ENTER_KEY else key_width) + key_rect = rl.Rectangle(start_x, row_y_start + row * (key_height + v_space), new_width, key_height) + start_x += new_width + + if gui_button(key_rect, key): + if key == ENTER_KEY: + return 1 + else: + self.handle_key_press(key) + + return 0 + + def handle_key_press(self, key): + if key in (SHIFT_DOWN_KEY, ABC_KEY): + self._layout = keyboard_layouts["lowercase"] + elif key == SHIFT_KEY: + self._layout = keyboard_layouts["uppercase"] + elif key == NUMERIC_KEY: + self._layout = keyboard_layouts["numbers"] + elif key == SYMBOL_KEY: + self._layout = keyboard_layouts["specials"] + elif key == BACKSPACE_KEY and len(self._input_text) > 0: + self._input_text = self._input_text[:-1] + elif key != BACKSPACE_KEY and len(self._input_text) < self._max_text_size: + self._input_text += key diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py new file mode 100644 index 0000000000..f7d0e363da --- /dev/null +++ b/system/ui/lib/wifi_manager.py @@ -0,0 +1,376 @@ +import asyncio +from dbus_next.aio import MessageBus +from dbus_next import BusType, Variant, Message +from dbus_next.errors import DBusError +from dbus_next.constants import MessageType +from enum import IntEnum +import uuid +from dataclasses import dataclass +from openpilot.common.swaglog import cloudlog + +NM = "org.freedesktop.NetworkManager" +NM_PATH = '/org/freedesktop/NetworkManager' +NM_IFACE = 'org.freedesktop.NetworkManager' +NM_SETTINGS_PATH = '/org/freedesktop/NetworkManager/Settings' +NM_SETTINGS_IFACE = 'org.freedesktop.NetworkManager.Settings' +NM_CONNECTION_IFACE = 'org.freedesktop.NetworkManager.Settings.Connection' +NM_WIRELESS_IFACE = 'org.freedesktop.NetworkManager.Device.Wireless' +NM_PROPERTIES_IFACE = 'org.freedesktop.DBus.Properties' +NM_DEVICE_IFACE = "org.freedesktop.NetworkManager.Device" + + +# NetworkManager device states +class NMDeviceState(IntEnum): + DISCONNECTED = 30 + PREPARE = 40 + NEED_AUTH = 60 + IP_CONFIG = 70 + ACTIVATED = 100 + + +class SecurityType(IntEnum): + OPEN = 0 + WPA = 1 + WPA2 = 2 + UNSUPPORTED = 3 + + +@dataclass +class NetworkInfo: + ssid: str + strength: int + is_connected: bool + security_type: SecurityType + path: str + bssid: str + # saved_path: str + + +class WifiManager: + def __init__(self): + self.networks: list[NetworkInfo] = [] + self.bus: MessageBus | None = None + self.device_path: str | None = None + self.device_proxy = None + self.saved_connections: dict[str, str] = dict() + self.active_ap_path: str = '' + self.scan_task: asyncio.Task | None = None + self.running: bool = True + + def is_saved(self, ssid: str) -> bool: + return ssid in self.saved_connections + + async def connect(self): + """Connect to the DBus system bus.""" + try: + self.bus = await MessageBus(bus_type=BusType.SYSTEM).connect() + if not await self._find_wifi_device(): + raise ValueError("No Wi-Fi device found") + await self._setup_signals(self.device_path) + + self.active_ap_path = await self.get_active_access_point() + self.saved_connections = await self._get_saved_connections() + self.scan_task = asyncio.create_task(self._periodic_scan()) + except DBusError as e: + cloudlog.error(f"Failed to connect to DBus: {e}") + raise + except Exception as e: + cloudlog.error(f"Unexpected error during connect: {e}") + raise + + async def shutdown(self) -> None: + self.running = False + if self.scan_task: + self.scan_task.cancel() + await self.scan_task + if self.bus: + await self.bus.disconnect() + + async def request_scan(self): + try: + interface = self.device_proxy.get_interface(NM_WIRELESS_IFACE) + await interface.call_request_scan({}) + except DBusError as e: + cloudlog.warning(f"Scan request failed: {e}") + + async def get_active_access_point(self): + try: + props_iface = self.device_proxy.get_interface(NM_PROPERTIES_IFACE) + ap_path = await props_iface.call_get(NM_WIRELESS_IFACE, 'ActiveAccessPoint') + return ap_path.value + except DBusError as e: + cloudlog.error(f"Error fetching active access point: {e}") + return '' + + async def forgot_connection(self, ssid: str) -> bool: + path = self.saved_connections.get(ssid) + if not path: + return False + + try: + nm_iface = await self._get_interface(NM, path, NM_CONNECTION_IFACE) + await nm_iface.call_delete() + self.saved_connections.pop(ssid) + return True + except DBusError as e: + cloudlog.error(f"Failed to delete connection for SSID: {ssid}. Error: {e}") + return False + except Exception as e: + cloudlog.error(f"Unexpected error while deleting connection for SSID: {ssid}: {e}") + return False + + async def activate_connection(self, ssid: str) -> None: + connection_path = self.saved_connections.get(ssid) + if connection_path: + cloudlog.info('activate connection:', connection_path) + introspection = await self.bus.introspect(NM, NM_PATH) + proxy = self.bus.get_proxy_object(NM, NM_PATH, introspection) + interface = proxy.get_interface(NM_IFACE) + + await interface.call_activate_connection(connection_path, self.device_path, '/') + + async def connect_to_network(self, ssid: str, password: str = None, is_hidden: bool = False): + """Connect to a selected WiFi network.""" + try: + settings_iface = await self._get_interface(NM, NM_SETTINGS_PATH, NM_SETTINGS_IFACE) + connection = { + 'connection': { + 'type': Variant('s', '802-11-wireless'), + 'uuid': Variant('s', str(uuid.uuid4())), + 'id': Variant('s', ssid), + 'autoconnect-retries': Variant('i', 0), + }, + '802-11-wireless': { + 'ssid': Variant('ay', ssid.encode('utf-8')), + 'hidden': Variant('b', is_hidden), + 'mode': Variant('s', 'infrastructure'), + }, + 'ipv4': {'method': Variant('s', 'auto')}, + 'ipv6': {'method': Variant('s', 'ignore')}, + } + + # if bssid: + # connection['802-11-wireless']['bssid'] = Variant('ay', bssid.encode('utf-8')) + + if password: + connection['802-11-wireless-security'] = { + 'key-mgmt': Variant('s', 'wpa-psk'), + 'auth-alg': Variant('s', 'open'), + 'psk': Variant('s', password), + } + + await settings_iface.call_add_connection(connection) + + for network in self.networks: + network.is_connected = True if network.ssid == ssid else False + + except DBusError as e: + cloudlog.error(f"Error connecting to network: {e}") + + async def _find_wifi_device(self) -> bool: + nm_iface = await self._get_interface(NM, NM_PATH, NM_IFACE) + devices = await nm_iface.get_devices() + + for device_path in devices: + device = await self.bus.introspect(NM, device_path) + device_proxy = self.bus.get_proxy_object(NM, device_path, device) + device_interface = device_proxy.get_interface(NM_DEVICE_IFACE) + device_type = await device_interface.get_device_type() + if device_type == 2: # WiFi device + self.device_path = device_path + self.device_proxy = device_proxy + return True + + return False + + async def _periodic_scan(self): + while self.running: + try: + await self.request_scan() + await self._get_available_networks() + await asyncio.sleep(30) + except asyncio.CancelledError: + break + except DBusError as e: + cloudlog.error(f"Scan failed: {e}") + await asyncio.sleep(5) + + async def _setup_signals(self, device_path: str) -> None: + rules = [ + f"type='signal',interface='{NM_PROPERTIES_IFACE}',member='PropertiesChanged',path='{device_path}'", + f"type='signal',interface='{NM_DEVICE_IFACE}',member='StateChanged',path='{device_path}'", + f"type='signal',interface='{NM_SETTINGS_IFACE}',member='NewConnection',path='{NM_SETTINGS_PATH}'", + f"type='signal',interface='{NM_SETTINGS_IFACE}',member='ConnectionRemoved',path='{NM_SETTINGS_PATH}'", + ] + for rule in rules: + await self._add_match_rule(rule) + + # Set up signal handlers + self.device_proxy.get_interface(NM_PROPERTIES_IFACE).on_properties_changed( + self._on_properties_changed + ) + self.device_proxy.get_interface(NM_DEVICE_IFACE).on_state_changed(self._on_state_changed) + + settings_iface = await self._get_interface(NM, NM_SETTINGS_PATH, NM_SETTINGS_IFACE) + settings_iface.on_new_connection(self._on_new_connection) + settings_iface.on_connection_removed(self._on_connection_removed) + + def _on_properties_changed(self, interface: str, changed: dict, invalidated: list): + # print("property changed", interface, changed, invalidated) + if 'LastScan' in changed: + asyncio.create_task(self._get_available_networks()) + elif "ActiveAccessPoint" in changed: + self.active_ap_path = changed["ActiveAccessPoint"].value + asyncio.create_task(self._get_available_networks()) + + def _on_state_changed(self, new_state: int, old_state: int, reason: int): + print(f"State changed: {old_state} -> {new_state}, reason: {reason}") + if new_state == NMDeviceState.ACTIVATED: + asyncio.create_task(self._update_connection_status()) + elif new_state in (NMDeviceState.DISCONNECTED, NMDeviceState.NEED_AUTH): + for network in self.networks: + network.is_connected = False + + def _on_new_connection(self, path: str) -> None: + """Callback for NewConnection signal.""" + print(f"New connection added: {path}") + asyncio.create_task(self._add_saved_connection(path)) + + def _on_connection_removed(self, path: str) -> None: + """Callback for ConnectionRemoved signal.""" + print(f"Connection removed: {path}") + for ssid, p in self.saved_connections.items(): + if path == p: + del self.saved_connections[ssid] + break + + async def _add_saved_connection(self, path: str) -> None: + """Add a new saved connection to the dictionary.""" + try: + settings = await self._get_connection_settings(path) + if ssid := self._extract_ssid(settings): + self.saved_connections[ssid] = path + except DBusError as e: + cloudlog.error(f"Failed to add connection {path}: {e}") + + def _extract_ssid(self, settings: dict) -> str | None: + """Extract SSID from connection settings.""" + ssid_variant = settings.get('802-11-wireless', {}).get('ssid', Variant('ay', b'')).value + return ''.join(chr(b) for b in ssid_variant) if ssid_variant else None + + async def _update_connection_status(self): + self.active_ap_path = await self.get_active_access_point() + await self._get_available_networks() + + async def _add_match_rule(self, rule): + """ "Add a match rule on the bus.""" + reply = await self.bus.call( + Message( + message_type=MessageType.METHOD_CALL, + destination='org.freedesktop.DBus', + interface="org.freedesktop.DBus", + path='/org/freedesktop/DBus', + member='AddMatch', + signature='s', + body=[rule], + ) + ) + + assert reply.message_type == MessageType.METHOD_RETURN + return reply + + async def _get_available_networks(self): + """Get a list of available networks via NetworkManager.""" + networks = [] + wifi_iface = self.device_proxy.get_interface(NM_WIRELESS_IFACE) + access_points = await wifi_iface.get_access_points() + + for ap_path in access_points: + try: + props_iface = await self._get_interface(NM, ap_path, NM_PROPERTIES_IFACE) + properties = await props_iface.call_get_all('org.freedesktop.NetworkManager.AccessPoint') + ssid_variant = properties['Ssid'].value + ssid = ''.join(chr(byte) for byte in ssid_variant) + if not ssid: + continue + + bssid = properties.get('HwAddress', Variant('s', '')).value + print(bssid) + flags = properties['Flags'].value + wpa_flags = properties['WpaFlags'].value + rsn_flags = properties['RsnFlags'].value + + networks.append( + NetworkInfo( + ssid=ssid, + strength=properties['Strength'].value, + security_type=self._get_security_type(flags, wpa_flags, rsn_flags), + path=ap_path, + bssid=bssid, + is_connected=self.active_ap_path == ap_path, + ) + ) + except DBusError as e: + cloudlog.error(f"Error fetching networks: {e}") + except Exception as e: + cloudlog.error({e}) + + self.networks = sorted( + networks, + key=lambda network: ( + not network.is_connected, + -network.strength, # Higher signal strength first + network.ssid.lower(), + ), + ) + + async def _get_connection_settings(self, path): + """Fetch connection settings for a specific connection path.""" + connection_proxy = await self.bus.introspect(NM, path) + connection = self.bus.get_proxy_object(NM, path, connection_proxy) + settings = connection.get_interface(NM_CONNECTION_IFACE) + return await settings.call_get_settings() + + async def _process_chunk(self, paths_chunk): + """Process a chunk of connection paths.""" + tasks = [self._get_connection_settings(path) for path in paths_chunk] + results = await asyncio.gather(*tasks) + return results + + async def _get_saved_connections(self): + settings_iface = await self._get_interface(NM, NM_SETTINGS_PATH, NM_SETTINGS_IFACE) + connection_paths = await settings_iface.call_list_connections() + + saved_ssids: dict[str, str] = {} + batch_size = 120 + for i in range(0, len(connection_paths), batch_size): + chunk = connection_paths[i : i + batch_size] + results = await self._process_chunk(chunk) + + # Loop through the results and filter Wi-Fi connections + for path, config in zip(chunk, results, strict=True): + if '802-11-wireless' in config: + saved_ssids[self._extract_ssid(config)] = path + + return saved_ssids + + async def _get_interface(self, bus_name: str, path: str, name: str): + introspection = await self.bus.introspect(bus_name, path) + proxy = self.bus.get_proxy_object(bus_name, path, introspection) + return proxy.get_interface(name) + + def _get_security_type(self, flags, wpa_flags, rsn_flags): + """Helper function to determine the security type of a network.""" + if flags == 0: + return SecurityType.OPEN + if wpa_flags: + return SecurityType.WPA + if rsn_flags: + return SecurityType.WPA2 + else: + return SecurityType.UNSUPPORTED + + async def _get_interface(self, bus_name: str, path: str, name: str): + introspection = await self.bus.introspect(bus_name, path) + proxy = self.bus.get_proxy_object(bus_name, path, introspection) + return proxy.get_interface(name) diff --git a/system/ui/network.py b/system/ui/network.py new file mode 100644 index 0000000000..cdb6d71fdd --- /dev/null +++ b/system/ui/network.py @@ -0,0 +1,158 @@ +import asyncio +import pyray as rl +from enum import IntEnum +from dbus_next.constants import MessageType +from openpilot.system.ui.lib.wifi_manager import WifiManager, NetworkInfo +from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.lib.label import gui_label +from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel +from openpilot.system.ui.lib.keyboard import Keyboard + +NM_DEVICE_STATE_NEED_AUTH = 60 + + +class ActionState(IntEnum): + NONE = 0 + CONNECT = 1 + CONNECTING = 2 + FORGOT = 3 + FORGETTING = 4 + NEED_AUTH = 5 + + +class WifiManagerUI: + def __init__(self, wifi_manager): + self.wifi_manager = wifi_manager + self._selected_network = None + self.item_height = 160 + self.btn_width = 200 + self.current_action: ActionState = ActionState.NONE + self.scroll_panel = GuiScrollPanel() + self.keyboard = Keyboard() + + asyncio.create_task(self._initialize()) + + async def _initialize(self) -> None: + try: + await self.wifi_manager.connect() + self.wifi_manager.bus.add_message_handler(self._handle_dbus_signal) + except Exception as e: + print(f"Initialization error: {e}") + + def draw_network_list(self, rect: rl.Rectangle): + if not self.wifi_manager.networks: + gui_label( + rect, "Scanning Wi-Fi networks...", 40, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER + ) + return + + if self.current_action == ActionState.NEED_AUTH: + result = self.keyboard.render(rect, 'Enter password', f'for {self._selected_network.ssid}') + if result == 0: + return + else: + self.current_action = ActionState.NONE + asyncio.create_task(self.connect_to_network(self.keyboard.text)) + + content_rect = rl.Rectangle( + rect.x, rect.y, rect.width, len(self.wifi_manager.networks) * self.item_height + ) + offset = self.scroll_panel.handle_scroll(rect, content_rect) + rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height)) + clicked = offset.y < 10 and rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) + for i, network in enumerate(self.wifi_manager.networks): + y_offset = i * self.item_height + offset.y + item_rect = rl.Rectangle(rect.x, y_offset, rect.width, self.item_height) + + if rl.check_collision_recs(item_rect, rect): + self.render_network_item(item_rect, network, clicked) + if i < len(self.wifi_manager.networks) - 1: + line_y = int(item_rect.y + item_rect.height - 1) + rl.draw_line( + int(item_rect.x), int(line_y), int(item_rect.x + item_rect.width), line_y, rl.LIGHTGRAY + ) + + rl.end_scissor_mode() + + def render_network_item(self, rect, network: NetworkInfo, clicked: bool): + label_rect = rl.Rectangle(rect.x, rect.y, rect.width - self.btn_width * 2, self.item_height) + state_rect = rl.Rectangle( + rect.x + rect.width - self.btn_width * 2 - 30, rect.y, self.btn_width, self.item_height + ) + + gui_label(label_rect, network.ssid, 55) + + if network.is_connected and self.current_action == ActionState.NONE: + rl.gui_label(state_rect, "Connected") + elif ( + self.current_action == "Connecting" + and self._selected_network + and self._selected_network.ssid == network.ssid + ): + rl.gui_label(state_rect, "CONNECTING...") + + # If the network is saved, show the "Forget" button + if self.wifi_manager.is_saved(network.ssid): + forget_btn_rect = rl.Rectangle( + rect.x + rect.width - self.btn_width, + rect.y + (self.item_height - 80) / 2, + self.btn_width, + 80, + ) + if rl.gui_button(forget_btn_rect, "Forget") and self.current_action == ActionState.NONE: + self._selected_network = network + asyncio.create_task(self.forgot_network()) + + if ( + self.current_action == ActionState.NONE + and rl.check_collision_point_rec(rl.get_mouse_position(), label_rect) + and clicked + ): + self._selected_network = network + if not self.wifi_manager.is_saved(self._selected_network.ssid): + self.current_action = ActionState.NEED_AUTH + else: + asyncio.create_task(self.connect_to_network()) + + async def forgot_network(self): + self.current_action = ActionState.FORGETTING + await self.wifi_manager.forgot_connection(self._selected_network.ssid) + self.current_action = ActionState.NONE + + async def connect_to_network(self, password=''): + self.current_action = ActionState.CONNECTING + if self.wifi_manager.is_saved(self._selected_network.ssid) and not password: + await self.wifi_manager.activate_connection(self._selected_network.ssid) + else: + await self.wifi_manager.connect_to_network(self._selected_network.ssid, password) + self.current_action = ActionState.NONE + + def _handle_dbus_signal(self, message): + if message.message_type != MessageType.SIGNAL: + return + + if message.member == 'StateChanged': + if len(message.body) >= 2: + _, new_state = message.body[0], message.body[1] + if new_state == NM_DEVICE_STATE_NEED_AUTH: + self.current_action = ActionState.NEED_AUTH + + +async def main(): + gui_app.init_window("Wifi Manager") + + wifi_manager = WifiManager() + wifi_ui = WifiManagerUI(wifi_manager) + + while not rl.window_should_close(): + rl.begin_drawing() + rl.clear_background(rl.BLACK) + + wifi_ui.draw_network_list(rl.Rectangle(50, 50, gui_app.width - 100, gui_app.height - 100)) + + rl.end_drawing() + await asyncio.sleep(0.001) + + +if __name__ == "__main__": + asyncio.run(main()) From a4b5022bea46bd8631596379b356ec86631f49bd Mon Sep 17 00:00:00 2001 From: deanlee Date: Sat, 8 Mar 2025 15:56:23 +0800 Subject: [PATCH 02/16] fix ui --- system/ui/lib/keyboard.py | 4 ++-- system/ui/lib/wifi_manager.py | 21 +++++++++++++-------- system/ui/network.py | 5 +++-- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/system/ui/lib/keyboard.py b/system/ui/lib/keyboard.py index 81e0d95754..4d1ad1b2cd 100644 --- a/system/ui/lib/keyboard.py +++ b/system/ui/lib/keyboard.py @@ -55,8 +55,8 @@ class Keyboard: self._input_text = "" def render(self, rect, title, sub_title): - gui_label((rect.x, rect.y, rect.width, 95), title, 90) - gui_label((rect.x, rect.y + 95, rect.width, 60), sub_title, 55, rl.GRAY) + gui_label(rl.Rectangle(rect.x, rect.y, rect.width, 95), title, 90) + gui_label(rl.Rectangle(rect.x, rect.y + 95, rect.width, 60), sub_title, 55, rl.GRAY) if gui_button(rl.Rectangle(rect.x + rect.width - 300, rect.y, 300, 100), "Cancel"): return -1 diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index f7d0e363da..17be8522a1 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -57,9 +57,6 @@ class WifiManager: self.scan_task: asyncio.Task | None = None self.running: bool = True - def is_saved(self, ssid: str) -> bool: - return ssid in self.saved_connections - async def connect(self): """Connect to the DBus system bus.""" try: @@ -110,7 +107,6 @@ class WifiManager: try: nm_iface = await self._get_interface(NM, path, NM_CONNECTION_IFACE) await nm_iface.call_delete() - self.saved_connections.pop(ssid) return True except DBusError as e: cloudlog.error(f"Failed to delete connection for SSID: {ssid}. Error: {e}") @@ -122,7 +118,7 @@ class WifiManager: async def activate_connection(self, ssid: str) -> None: connection_path = self.saved_connections.get(ssid) if connection_path: - cloudlog.info('activate connection:', connection_path) + print('activate connection:', connection_path) introspection = await self.bus.introspect(NM, NM_PATH) proxy = self.bus.get_proxy_object(NM, NM_PATH, introspection) interface = proxy.get_interface(NM_IFACE) @@ -132,7 +128,7 @@ class WifiManager: async def connect_to_network(self, ssid: str, password: str = None, is_hidden: bool = False): """Connect to a selected WiFi network.""" try: - settings_iface = await self._get_interface(NM, NM_SETTINGS_PATH, NM_SETTINGS_IFACE) + # settings_iface = await self._get_interface(NM, NM_SETTINGS_PATH, NM_SETTINGS_IFACE) connection = { 'connection': { 'type': Variant('s', '802-11-wireless'), @@ -159,7 +155,14 @@ class WifiManager: 'psk': Variant('s', password), } - await settings_iface.call_add_connection(connection) + # nm_iface = self._get_interface(NM, NM_PATH, NM_IFACE) + # await nm_iface.call_add_and_activate_connection(connection, self.device_path, '/') + # await settings_iface.call_add_connection(connection) + # introspection = await self.bus.introspect(NM, NM_PATH) + # nm_proxy = self.bus.get_proxy_object(NM, NM_PATH, introspection) + nm_iface = await self._get_interface(NM, NM_PATH, NM_IFACE) + result = await nm_iface.call_add_and_activate_connection(connection, self.device_path, "/") + print(result) for network in self.networks: network.is_connected = True if network.ssid == ssid else False @@ -167,6 +170,9 @@ class WifiManager: except DBusError as e: cloudlog.error(f"Error connecting to network: {e}") + def is_saved(self, ssid: str) -> bool: + return ssid in self.saved_connections + async def _find_wifi_device(self) -> bool: nm_iface = await self._get_interface(NM, NM_PATH, NM_IFACE) devices = await nm_iface.get_devices() @@ -295,7 +301,6 @@ class WifiManager: continue bssid = properties.get('HwAddress', Variant('s', '')).value - print(bssid) flags = properties['Flags'].value wpa_flags = properties['WpaFlags'].value rsn_flags = properties['RsnFlags'].value diff --git a/system/ui/network.py b/system/ui/network.py index cdb6d71fdd..bbbb09a333 100644 --- a/system/ui/network.py +++ b/system/ui/network.py @@ -4,6 +4,7 @@ from enum import IntEnum from dbus_next.constants import MessageType from openpilot.system.ui.lib.wifi_manager import WifiManager, NetworkInfo from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.lib.button import gui_button from openpilot.system.ui.lib.label import gui_label from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.keyboard import Keyboard @@ -77,7 +78,7 @@ class WifiManagerUI: def render_network_item(self, rect, network: NetworkInfo, clicked: bool): label_rect = rl.Rectangle(rect.x, rect.y, rect.width - self.btn_width * 2, self.item_height) state_rect = rl.Rectangle( - rect.x + rect.width - self.btn_width * 2 - 30, rect.y, self.btn_width, self.item_height + rect.x + rect.width - self.btn_width * 2 - 150, rect.y, 300, self.item_height ) gui_label(label_rect, network.ssid, 55) @@ -99,7 +100,7 @@ class WifiManagerUI: self.btn_width, 80, ) - if rl.gui_button(forget_btn_rect, "Forget") and self.current_action == ActionState.NONE: + if gui_button(forget_btn_rect, "Forget") and self.current_action == ActionState.NONE: self._selected_network = network asyncio.create_task(self.forgot_network()) From 7dc82abb1c01359b0868221bdb951433b88dea3b Mon Sep 17 00:00:00 2001 From: deanlee Date: Sun, 9 Mar 2025 06:47:03 +0800 Subject: [PATCH 03/16] need auth callback --- system/ui/lib/wifi_manager.py | 11 +++++------ system/ui/network.py | 17 ++++++----------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 17be8522a1..e767cfd052 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -56,6 +56,7 @@ class WifiManager: self.active_ap_path: str = '' self.scan_task: asyncio.Task | None = None self.running: bool = True + self.need_auth_callback = None async def connect(self): """Connect to the DBus system bus.""" @@ -118,12 +119,8 @@ class WifiManager: async def activate_connection(self, ssid: str) -> None: connection_path = self.saved_connections.get(ssid) if connection_path: - print('activate connection:', connection_path) - introspection = await self.bus.introspect(NM, NM_PATH) - proxy = self.bus.get_proxy_object(NM, NM_PATH, introspection) - interface = proxy.get_interface(NM_IFACE) - - await interface.call_activate_connection(connection_path, self.device_path, '/') + nm_iface = await self._get_interface(NM, NM_PATH, NM_IFACE) + await nm_iface.call_activate_connection(connection_path, self.device_path, '/') async def connect_to_network(self, ssid: str, password: str = None, is_hidden: bool = False): """Connect to a selected WiFi network.""" @@ -236,6 +233,8 @@ class WifiManager: elif new_state in (NMDeviceState.DISCONNECTED, NMDeviceState.NEED_AUTH): for network in self.networks: network.is_connected = False + if new_state == NMDeviceState.NEED_AUTH and self.need_auth_callback: + self.need_auth_callback() def _on_new_connection(self, path: str) -> None: """Callback for NewConnection signal.""" diff --git a/system/ui/network.py b/system/ui/network.py index bbbb09a333..436c1b2c04 100644 --- a/system/ui/network.py +++ b/system/ui/network.py @@ -24,6 +24,7 @@ class ActionState(IntEnum): class WifiManagerUI: def __init__(self, wifi_manager): self.wifi_manager = wifi_manager + self.wifi_manager.need_auth_callback = self._need_auth self._selected_network = None self.item_height = 160 self.btn_width = 200 @@ -36,7 +37,6 @@ class WifiManagerUI: async def _initialize(self) -> None: try: await self.wifi_manager.connect() - self.wifi_manager.bus.add_message_handler(self._handle_dbus_signal) except Exception as e: print(f"Initialization error: {e}") @@ -51,9 +51,11 @@ class WifiManagerUI: result = self.keyboard.render(rect, 'Enter password', f'for {self._selected_network.ssid}') if result == 0: return - else: + elif result == 1: self.current_action = ActionState.NONE asyncio.create_task(self.connect_to_network(self.keyboard.text)) + else: + self.current_action = ActionState.NONE content_rect = rl.Rectangle( rect.x, rect.y, rect.width, len(self.wifi_manager.networks) * self.item_height @@ -128,15 +130,8 @@ class WifiManagerUI: await self.wifi_manager.connect_to_network(self._selected_network.ssid, password) self.current_action = ActionState.NONE - def _handle_dbus_signal(self, message): - if message.message_type != MessageType.SIGNAL: - return - - if message.member == 'StateChanged': - if len(message.body) >= 2: - _, new_state = message.body[0], message.body[1] - if new_state == NM_DEVICE_STATE_NEED_AUTH: - self.current_action = ActionState.NEED_AUTH + def _need_auth(self): + self.current_action = ActionState.NEED_AUTH async def main(): From 982ca78c8834e6f0cd5b31ee1208ecb84a2576d8 Mon Sep 17 00:00:00 2001 From: deanlee Date: Mon, 10 Mar 2025 09:06:21 +0800 Subject: [PATCH 04/16] move to widgets --- system/ui/lib/keyboard.py | 104 ----------------------------- system/ui/{ => widgets}/network.py | 2 +- 2 files changed, 1 insertion(+), 105 deletions(-) delete mode 100644 system/ui/lib/keyboard.py rename system/ui/{ => widgets}/network.py (98%) diff --git a/system/ui/lib/keyboard.py b/system/ui/lib/keyboard.py deleted file mode 100644 index 4d1ad1b2cd..0000000000 --- a/system/ui/lib/keyboard.py +++ /dev/null @@ -1,104 +0,0 @@ -import pyray as rl -from openpilot.system.ui.lib.button import gui_button -from openpilot.system.ui.lib.label import gui_label - -# Constants for special keys -BACKSPACE_KEY = "<-" -ENTER_KEY = "Enter" -SPACE_KEY = " " -SHIFT_KEY = "↑" -SHIFT_DOWN_KEY = "↓" -NUMERIC_KEY = "123" -SYMBOL_KEY = "#+=" -ABC_KEY = "ABC" - -# Define keyboard layouts as a dictionary for easier access -keyboard_layouts = { - "lowercase": [ - ["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"], - ["a", "s", "d", "f", "g", "h", "j", "k", "l"], - [SHIFT_KEY, "z", "x", "c", "v", "b", "n", "m", BACKSPACE_KEY], - [NUMERIC_KEY, "/", "-", SPACE_KEY, ".", ENTER_KEY], - ], - "uppercase": [ - ["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"], - ["A", "S", "D", "F", "G", "H", "J", "K", "L"], - [SHIFT_DOWN_KEY, "Z", "X", "C", "V", "B", "N", "M", BACKSPACE_KEY], - [NUMERIC_KEY, "/", "-", SPACE_KEY, ".", ENTER_KEY], - ], - "numbers": [ - ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"], - ["-", "/", ":", ";", "(", ")", "$", "&", "@", "\""], - [SYMBOL_KEY, ".", ",", "?", "!", "`", BACKSPACE_KEY], - [ABC_KEY, SPACE_KEY, ".", ENTER_KEY], - ], - "specials": [ - ["[", "]", "{", "}", "#", "%", "^", "*", "+", "="], - ["_", "\\", "|", "~", "<", ">", "€", "£", "¥", "•"], - [NUMERIC_KEY, ".", ",", "?", "!", "'", BACKSPACE_KEY], - [ABC_KEY, SPACE_KEY, ".", ENTER_KEY], - ], -} - - -class Keyboard: - def __init__(self, max_text_size: int = 255): - self._layout = keyboard_layouts["lowercase"] - self._input_text = "" - self._max_text_size = max_text_size - - @property - def text(self) -> str: - return self._input_text - - def clear(self): - self._input_text = "" - - def render(self, rect, title, sub_title): - gui_label(rl.Rectangle(rect.x, rect.y, rect.width, 95), title, 90) - gui_label(rl.Rectangle(rect.x, rect.y + 95, rect.width, 60), sub_title, 55, rl.GRAY) - if gui_button(rl.Rectangle(rect.x + rect.width - 300, rect.y, 300, 100), "Cancel"): - return -1 - - # Text box for input - rl.gui_text_box(rl.Rectangle(rect.x, rect.y + 160, rect.width, 100), self._input_text, self._max_text_size, True) - - h_space, v_space = 15, 15 - row_y_start = rect.y + 300 # Starting Y position for the first row - key_height = (rect.height - 300 - 3 * v_space) / 4 - key_max_width = (rect.width - (len(self._layout[2]) - 1) * h_space) / len(self._layout[2]) - - # Iterate over the rows of keys in the current layout - for row, keys in enumerate(self._layout): - key_width = min((rect.width - (180 if row == 1 else 0) - h_space * (len(keys) - 1)) / len(keys), key_max_width) - start_x = rect.x + (90 if row == 1 else 0) - - for i, key in enumerate(keys): - if i > 0: - start_x += h_space - - new_width = (key_width * 3 + h_space * 2) if key == SPACE_KEY else (key_width * 2 + h_space if key == ENTER_KEY else key_width) - key_rect = rl.Rectangle(start_x, row_y_start + row * (key_height + v_space), new_width, key_height) - start_x += new_width - - if gui_button(key_rect, key): - if key == ENTER_KEY: - return 1 - else: - self.handle_key_press(key) - - return 0 - - def handle_key_press(self, key): - if key in (SHIFT_DOWN_KEY, ABC_KEY): - self._layout = keyboard_layouts["lowercase"] - elif key == SHIFT_KEY: - self._layout = keyboard_layouts["uppercase"] - elif key == NUMERIC_KEY: - self._layout = keyboard_layouts["numbers"] - elif key == SYMBOL_KEY: - self._layout = keyboard_layouts["specials"] - elif key == BACKSPACE_KEY and len(self._input_text) > 0: - self._input_text = self._input_text[:-1] - elif key != BACKSPACE_KEY and len(self._input_text) < self._max_text_size: - self._input_text += key diff --git a/system/ui/network.py b/system/ui/widgets/network.py similarity index 98% rename from system/ui/network.py rename to system/ui/widgets/network.py index 436c1b2c04..11c096cf36 100644 --- a/system/ui/network.py +++ b/system/ui/widgets/network.py @@ -7,7 +7,7 @@ from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.button import gui_button from openpilot.system.ui.lib.label import gui_label from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel -from openpilot.system.ui.lib.keyboard import Keyboard +from openpilot.system.ui.widgets.keyboard import Keyboard NM_DEVICE_STATE_NEED_AUTH = 60 From 152c285e10297eda6ea66f6f8d0bfbb18bb62210 Mon Sep 17 00:00:00 2001 From: deanlee Date: Mon, 10 Mar 2025 10:27:49 +0800 Subject: [PATCH 05/16] confirm forgot --- system/ui/lib/wifi_manager.py | 14 +++++--------- system/ui/widgets/network.py | 28 ++++++++++++++++++++-------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index e767cfd052..618e590103 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -8,6 +8,7 @@ import uuid from dataclasses import dataclass from openpilot.common.swaglog import cloudlog +# NetworkManager constants NM = "org.freedesktop.NetworkManager" NM_PATH = '/org/freedesktop/NetworkManager' NM_IFACE = 'org.freedesktop.NetworkManager' @@ -152,17 +153,12 @@ class WifiManager: 'psk': Variant('s', password), } - # nm_iface = self._get_interface(NM, NM_PATH, NM_IFACE) - # await nm_iface.call_add_and_activate_connection(connection, self.device_path, '/') - # await settings_iface.call_add_connection(connection) - # introspection = await self.bus.introspect(NM, NM_PATH) - # nm_proxy = self.bus.get_proxy_object(NM, NM_PATH, introspection) nm_iface = await self._get_interface(NM, NM_PATH, NM_IFACE) - result = await nm_iface.call_add_and_activate_connection(connection, self.device_path, "/") - print(result) + await nm_iface.call_add_and_activate_connection(connection, self.device_path, "/") - for network in self.networks: - network.is_connected = True if network.ssid == ssid else False + # for network in self.networks: + # network.is_connected = True if network.ssid == ssid else False + await self._update_connection_status() except DBusError as e: cloudlog.error(f"Error connecting to network: {e}") diff --git a/system/ui/widgets/network.py b/system/ui/widgets/network.py index 11c096cf36..2aae1830b3 100644 --- a/system/ui/widgets/network.py +++ b/system/ui/widgets/network.py @@ -8,6 +8,7 @@ from openpilot.system.ui.lib.button import gui_button from openpilot.system.ui.lib.label import gui_label from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.widgets.keyboard import Keyboard +from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog NM_DEVICE_STATE_NEED_AUTH = 60 @@ -19,6 +20,7 @@ class ActionState(IntEnum): FORGOT = 3 FORGETTING = 4 NEED_AUTH = 5 + SHOW_FORGOT_CONFIRM = 6 class WifiManagerUI: @@ -40,11 +42,17 @@ class WifiManagerUI: except Exception as e: print(f"Initialization error: {e}") - def draw_network_list(self, rect: rl.Rectangle): + def render(self, rect: rl.Rectangle): if not self.wifi_manager.networks: - gui_label( - rect, "Scanning Wi-Fi networks...", 40, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER - ) + gui_label(rect, "Scanning Wi-Fi networks...", 40, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) + return + + if self.current_action == ActionState.SHOW_FORGOT_CONFIRM: + result = confirm_dialog(rect, f'Forget Wi-Fi Network "{self._selected_network.ssid}"?', 'Forget') + if result == 1: + asyncio.create_task(self.forgot_network()) + elif result == 0: + self.current_action = ActionState.NONE return if self.current_action == ActionState.NEED_AUTH: @@ -56,7 +64,11 @@ class WifiManagerUI: asyncio.create_task(self.connect_to_network(self.keyboard.text)) else: self.current_action = ActionState.NONE + return + self._draw_network_list(rect) + + def _draw_network_list(self, rect: rl.Rectangle): content_rect = rl.Rectangle( rect.x, rect.y, rect.width, len(self.wifi_manager.networks) * self.item_height ) @@ -68,7 +80,7 @@ class WifiManagerUI: item_rect = rl.Rectangle(rect.x, y_offset, rect.width, self.item_height) if rl.check_collision_recs(item_rect, rect): - self.render_network_item(item_rect, network, clicked) + self._draw_network_item(item_rect, network, clicked) if i < len(self.wifi_manager.networks) - 1: line_y = int(item_rect.y + item_rect.height - 1) rl.draw_line( @@ -77,7 +89,7 @@ class WifiManagerUI: rl.end_scissor_mode() - def render_network_item(self, rect, network: NetworkInfo, clicked: bool): + def _draw_network_item(self, rect, network: NetworkInfo, clicked: bool): label_rect = rl.Rectangle(rect.x, rect.y, rect.width - self.btn_width * 2, self.item_height) state_rect = rl.Rectangle( rect.x + rect.width - self.btn_width * 2 - 150, rect.y, 300, self.item_height @@ -104,7 +116,7 @@ class WifiManagerUI: ) if gui_button(forget_btn_rect, "Forget") and self.current_action == ActionState.NONE: self._selected_network = network - asyncio.create_task(self.forgot_network()) + self.current_action = ActionState.SHOW_FORGOT_CONFIRM if ( self.current_action == ActionState.NONE @@ -144,7 +156,7 @@ async def main(): rl.begin_drawing() rl.clear_background(rl.BLACK) - wifi_ui.draw_network_list(rl.Rectangle(50, 50, gui_app.width - 100, gui_app.height - 100)) + wifi_ui.render(rl.Rectangle(50, 50, gui_app.width - 100, gui_app.height - 100)) rl.end_drawing() await asyncio.sleep(0.001) From f0574d0aa15c0616561fa5502ef217e064d32e23 Mon Sep 17 00:00:00 2001 From: deanlee Date: Mon, 10 Mar 2025 14:16:39 +0800 Subject: [PATCH 06/16] add drag detection --- system/ui/lib/scroll_panel.py | 31 +++++++-- system/ui/lib/wifi_manager.py | 119 ++++++++++++++++++---------------- system/ui/widgets/network.py | 25 +++---- 3 files changed, 97 insertions(+), 78 deletions(-) diff --git a/system/ui/lib/scroll_panel.py b/system/ui/lib/scroll_panel.py index 0dc757ff6f..bd54f739ab 100644 --- a/system/ui/lib/scroll_panel.py +++ b/system/ui/lib/scroll_panel.py @@ -4,6 +4,7 @@ from enum import IntEnum MOUSE_WHEEL_SCROLL_SPEED = 30 INERTIA_FRICTION = 0.95 # The rate at which the inertia slows down MIN_VELOCITY = 0.1 # Minimum velocity before stopping the inertia +DRAG_THRESHOLD = 5 # Pixels of movement to consider it a drag, not a click class ScrollState(IntEnum): @@ -16,39 +17,52 @@ class GuiScrollPanel: def __init__(self, show_vertical_scroll_bar: bool = False): self._scroll_state: ScrollState = ScrollState.IDLE self._last_mouse_y: float = 0.0 + self._start_mouse_y: float = 0.0 # Track initial mouse position for drag detection self._offset = rl.Vector2(0, 0) self._view = rl.Rectangle(0, 0, 0, 0) self._show_vertical_scroll_bar: bool = show_vertical_scroll_bar self._velocity_y = 0.0 # Velocity for inertia + self._is_dragging = False # Flag to indicate if drag occurred def handle_scroll(self, bounds: rl.Rectangle, content: rl.Rectangle) -> rl.Vector2: mouse_pos = rl.get_mouse_position() # Handle dragging logic - if rl.check_collision_point_rec(mouse_pos, bounds) and rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT): + if rl.check_collision_point_rec(mouse_pos, bounds) and rl.is_mouse_button_pressed( + rl.MouseButton.MOUSE_BUTTON_LEFT + ): if self._scroll_state == ScrollState.IDLE: self._scroll_state = ScrollState.DRAGGING_CONTENT if self._show_vertical_scroll_bar: - scrollbar_width = rl.gui_get_style(rl.GuiControl.LISTVIEW, rl.GuiListViewProperty.SCROLLBAR_WIDTH) + scrollbar_width = rl.gui_get_style( + rl.GuiControl.LISTVIEW, rl.GuiListViewProperty.SCROLLBAR_WIDTH + ) scrollbar_x = bounds.x + bounds.width - scrollbar_width if mouse_pos.x >= scrollbar_x: self._scroll_state = ScrollState.DRAGGING_SCROLLBAR self._last_mouse_y = mouse_pos.y + self._start_mouse_y = mouse_pos.y # Record starting position self._velocity_y = 0.0 # Reset velocity when drag starts + self._is_dragging = False # Reset dragging flag if self._scroll_state != ScrollState.IDLE: if rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT): delta_y = mouse_pos.y - self._last_mouse_y + # Check if movement exceeds drag threshold + total_drag = abs(mouse_pos.y - self._start_mouse_y) + if total_drag > DRAG_THRESHOLD: + self._is_dragging = True + if self._scroll_state == ScrollState.DRAGGING_CONTENT: self._offset.y += delta_y - else: - delta_y = -delta_y + else: # DRAGGING_SCROLLBAR + delta_y = -delta_y # Invert for scrollbar self._last_mouse_y = mouse_pos.y self._velocity_y = delta_y # Update velocity during drag - else: + elif rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT): self._scroll_state = ScrollState.IDLE # Handle mouse wheel scrolling @@ -73,3 +87,10 @@ class GuiScrollPanel: self._offset.y = max(min(self._offset.y, 0), -max_scroll_y) return self._offset + + def is_click_valid(self) -> bool: + return ( + self._scroll_state == ScrollState.IDLE + and not self._is_dragging + and rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) + ) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 618e590103..b85812eb98 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -33,7 +33,8 @@ class SecurityType(IntEnum): OPEN = 0 WPA = 1 WPA2 = 2 - UNSUPPORTED = 3 + WPA3 = 3 + UNSUPPORTED = 4 @dataclass @@ -53,13 +54,13 @@ class WifiManager: self.bus: MessageBus | None = None self.device_path: str | None = None self.device_proxy = None - self.saved_connections: dict[str, str] = dict() + self.saved_connections: dict[str, str] = {} self.active_ap_path: str = '' self.scan_task: asyncio.Task | None = None self.running: bool = True self.need_auth_callback = None - async def connect(self): + async def connect(self) -> None: """Connect to the DBus system bus.""" try: self.bus = await MessageBus(bus_type=BusType.SYSTEM).connect() @@ -81,27 +82,30 @@ class WifiManager: self.running = False if self.scan_task: self.scan_task.cancel() - await self.scan_task + try: + await self.scan_task + except asyncio.CancelledError: + pass if self.bus: await self.bus.disconnect() - async def request_scan(self): + async def request_scan(self) -> None: try: interface = self.device_proxy.get_interface(NM_WIRELESS_IFACE) await interface.call_request_scan({}) except DBusError as e: - cloudlog.warning(f"Scan request failed: {e}") + cloudlog.warning(f"Scan request failed: {str(e)}") - async def get_active_access_point(self): + async def get_active_access_point(self) -> str: try: props_iface = self.device_proxy.get_interface(NM_PROPERTIES_IFACE) ap_path = await props_iface.call_get(NM_WIRELESS_IFACE, 'ActiveAccessPoint') return ap_path.value except DBusError as e: - cloudlog.error(f"Error fetching active access point: {e}") + cloudlog.error(f"Error fetching active access point: {str(e)}") return '' - async def forgot_connection(self, ssid: str) -> bool: + async def forget_connection(self, ssid: str) -> bool: path = self.saved_connections.get(ssid) if not path: return False @@ -113,17 +117,22 @@ class WifiManager: except DBusError as e: cloudlog.error(f"Failed to delete connection for SSID: {ssid}. Error: {e}") return False - except Exception as e: - cloudlog.error(f"Unexpected error while deleting connection for SSID: {ssid}: {e}") - return False - async def activate_connection(self, ssid: str) -> None: + async def activate_connection(self, ssid: str) -> bool: connection_path = self.saved_connections.get(ssid) - if connection_path: + if not connection_path: + return False + try: nm_iface = await self._get_interface(NM, NM_PATH, NM_IFACE) await nm_iface.call_activate_connection(connection_path, self.device_path, '/') + return True + except DBusError as e: + cloudlog.error(f"Failed to activate connection {ssid}: {str(e)}") + return False - async def connect_to_network(self, ssid: str, password: str = None, is_hidden: bool = False): + async def connect_to_network( + self, ssid: str, password: str = None, bssid: str = None, is_hidden: bool = False + ) -> None: """Connect to a selected WiFi network.""" try: # settings_iface = await self._get_interface(NM, NM_SETTINGS_PATH, NM_SETTINGS_IFACE) @@ -143,8 +152,8 @@ class WifiManager: 'ipv6': {'method': Variant('s', 'ignore')}, } - # if bssid: - # connection['802-11-wireless']['bssid'] = Variant('ay', bssid.encode('utf-8')) + if bssid: + connection['802-11-wireless']['bssid'] = Variant('ay', bssid.encode('utf-8')) if password: connection['802-11-wireless-security'] = { @@ -205,9 +214,7 @@ class WifiManager: await self._add_match_rule(rule) # Set up signal handlers - self.device_proxy.get_interface(NM_PROPERTIES_IFACE).on_properties_changed( - self._on_properties_changed - ) + self.device_proxy.get_interface(NM_PROPERTIES_IFACE).on_properties_changed(self._on_properties_changed) self.device_proxy.get_interface(NM_DEVICE_IFACE).on_state_changed(self._on_state_changed) settings_iface = await self._get_interface(NM, NM_SETTINGS_PATH, NM_SETTINGS_IFACE) @@ -240,7 +247,7 @@ class WifiManager: def _on_connection_removed(self, path: str) -> None: """Callback for ConnectionRemoved signal.""" print(f"Connection removed: {path}") - for ssid, p in self.saved_connections.items(): + for ssid, p in list(self.saved_connections.items()): if path == p: del self.saved_connections[ssid] break @@ -326,51 +333,51 @@ class WifiManager: async def _get_connection_settings(self, path): """Fetch connection settings for a specific connection path.""" - connection_proxy = await self.bus.introspect(NM, path) - connection = self.bus.get_proxy_object(NM, path, connection_proxy) - settings = connection.get_interface(NM_CONNECTION_IFACE) - return await settings.call_get_settings() + try: + connection_proxy = await self.bus.introspect(NM, path) + connection = self.bus.get_proxy_object(NM, path, connection_proxy) + settings = connection.get_interface(NM_CONNECTION_IFACE) + return await settings.call_get_settings() + except DBusError as e: + cloudlog.error(f"Failed to get settings for {path}: {str(e)}") + return {} async def _process_chunk(self, paths_chunk): """Process a chunk of connection paths.""" tasks = [self._get_connection_settings(path) for path in paths_chunk] - results = await asyncio.gather(*tasks) - return results + return await asyncio.gather(*tasks, return_exceptions=True) - async def _get_saved_connections(self): - settings_iface = await self._get_interface(NM, NM_SETTINGS_PATH, NM_SETTINGS_IFACE) - connection_paths = await settings_iface.call_list_connections() - - saved_ssids: dict[str, str] = {} - batch_size = 120 - for i in range(0, len(connection_paths), batch_size): - chunk = connection_paths[i : i + batch_size] - results = await self._process_chunk(chunk) - - # Loop through the results and filter Wi-Fi connections - for path, config in zip(chunk, results, strict=True): - if '802-11-wireless' in config: - saved_ssids[self._extract_ssid(config)] = path - - return saved_ssids + async def _get_saved_connections(self) -> dict[str, str]: + try: + settings_iface = await self._get_interface(NM, NM_SETTINGS_PATH, NM_SETTINGS_IFACE) + connection_paths = await settings_iface.call_list_connections() + saved_ssids: dict[str, str] = {} + batch_size = 20 + for i in range(0, len(connection_paths), batch_size): + chunk = connection_paths[i : i + batch_size] + results = await self._process_chunk(chunk) + for path, config in zip(chunk, results, strict=True): + if isinstance(config, dict) and '802-11-wireless' in config: + if ssid := self._extract_ssid(config): + saved_ssids[ssid] = path + return saved_ssids + except DBusError as e: + cloudlog.error(f"Error fetching saved connections: {str(e)}") + return {} async def _get_interface(self, bus_name: str, path: str, name: str): introspection = await self.bus.introspect(bus_name, path) proxy = self.bus.get_proxy_object(bus_name, path, introspection) return proxy.get_interface(name) - def _get_security_type(self, flags, wpa_flags, rsn_flags): - """Helper function to determine the security type of a network.""" - if flags == 0: + def _get_security_type(self, flags: int, wpa_flags: int, rsn_flags: int) -> SecurityType: + """Determine the security type based on flags.""" + if flags == 0 and not (wpa_flags or rsn_flags): return SecurityType.OPEN - if wpa_flags: - return SecurityType.WPA - if rsn_flags: + if rsn_flags & 0x200: # SAE (WPA3 Personal) + return SecurityType.WPA3 + if rsn_flags: # RSN indicates WPA2 or higher return SecurityType.WPA2 - else: - return SecurityType.UNSUPPORTED - - async def _get_interface(self, bus_name: str, path: str, name: str): - introspection = await self.bus.introspect(bus_name, path) - proxy = self.bus.get_proxy_object(bus_name, path, introspection) - return proxy.get_interface(name) + if wpa_flags: # WPA flags indicate WPA + return SecurityType.WPA + return SecurityType.UNSUPPORTED diff --git a/system/ui/widgets/network.py b/system/ui/widgets/network.py index 2aae1830b3..1aab7dceb6 100644 --- a/system/ui/widgets/network.py +++ b/system/ui/widgets/network.py @@ -69,12 +69,11 @@ class WifiManagerUI: self._draw_network_list(rect) def _draw_network_list(self, rect: rl.Rectangle): - content_rect = rl.Rectangle( - rect.x, rect.y, rect.width, len(self.wifi_manager.networks) * self.item_height - ) + content_rect = rl.Rectangle(rect.x, rect.y, rect.width, len(self.wifi_manager.networks) * self.item_height) offset = self.scroll_panel.handle_scroll(rect, content_rect) + clicked = self.scroll_panel.is_click_valid() + rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height)) - clicked = offset.y < 10 and rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) for i, network in enumerate(self.wifi_manager.networks): y_offset = i * self.item_height + offset.y item_rect = rl.Rectangle(rect.x, y_offset, rect.width, self.item_height) @@ -83,27 +82,19 @@ class WifiManagerUI: self._draw_network_item(item_rect, network, clicked) if i < len(self.wifi_manager.networks) - 1: line_y = int(item_rect.y + item_rect.height - 1) - rl.draw_line( - int(item_rect.x), int(line_y), int(item_rect.x + item_rect.width), line_y, rl.LIGHTGRAY - ) + rl.draw_line(int(item_rect.x), int(line_y), int(item_rect.x + item_rect.width), line_y, rl.LIGHTGRAY) rl.end_scissor_mode() def _draw_network_item(self, rect, network: NetworkInfo, clicked: bool): label_rect = rl.Rectangle(rect.x, rect.y, rect.width - self.btn_width * 2, self.item_height) - state_rect = rl.Rectangle( - rect.x + rect.width - self.btn_width * 2 - 150, rect.y, 300, self.item_height - ) + state_rect = rl.Rectangle(rect.x + rect.width - self.btn_width * 2 - 150, rect.y, 300, self.item_height) gui_label(label_rect, network.ssid, 55) if network.is_connected and self.current_action == ActionState.NONE: rl.gui_label(state_rect, "Connected") - elif ( - self.current_action == "Connecting" - and self._selected_network - and self._selected_network.ssid == network.ssid - ): + elif self.current_action == "Connecting" and self._selected_network and self._selected_network.ssid == network.ssid: rl.gui_label(state_rect, "CONNECTING...") # If the network is saved, show the "Forget" button @@ -114,7 +105,7 @@ class WifiManagerUI: self.btn_width, 80, ) - if gui_button(forget_btn_rect, "Forget") and self.current_action == ActionState.NONE: + if gui_button(forget_btn_rect, "Forget") and self.current_action == ActionState.NONE and clicked: self._selected_network = network self.current_action = ActionState.SHOW_FORGOT_CONFIRM @@ -131,7 +122,7 @@ class WifiManagerUI: async def forgot_network(self): self.current_action = ActionState.FORGETTING - await self.wifi_manager.forgot_connection(self._selected_network.ssid) + await self.wifi_manager.forget_connection(self._selected_network.ssid) self.current_action = ActionState.NONE async def connect_to_network(self, password=''): From 8baa066015fa4bb088e5704a25d4de09090f84d2 Mon Sep 17 00:00:00 2001 From: deanlee Date: Mon, 10 Mar 2025 17:20:58 +0800 Subject: [PATCH 07/16] improve keyboard & list --- system/ui/lib/wifi_manager.py | 2 +- system/ui/widgets/keyboard.py | 40 ++++++++++++++++++++++++++--------- system/ui/widgets/network.py | 35 ++++++++++++++---------------- 3 files changed, 47 insertions(+), 30 deletions(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index b85812eb98..44ac5e23fc 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -225,7 +225,7 @@ class WifiManager: # print("property changed", interface, changed, invalidated) if 'LastScan' in changed: asyncio.create_task(self._get_available_networks()) - elif "ActiveAccessPoint" in changed: + elif interface == NM_WIRELESS_IFACE and "ActiveAccessPoint" in changed: self.active_ap_path = changed["ActiveAccessPoint"].value asyncio.create_task(self._get_available_networks()) diff --git a/system/ui/widgets/keyboard.py b/system/ui/widgets/keyboard.py index 4d1ad1b2cd..4d970b46dc 100644 --- a/system/ui/widgets/keyboard.py +++ b/system/ui/widgets/keyboard.py @@ -44,25 +44,30 @@ keyboard_layouts = { class Keyboard: def __init__(self, max_text_size: int = 255): self._layout = keyboard_layouts["lowercase"] - self._input_text = "" self._max_text_size = max_text_size + self._string_pointer = rl.ffi.new("char[]", max_text_size) + self._input_text: str = '' + self._clear() @property def text(self) -> str: - return self._input_text - - def clear(self): - self._input_text = "" + result = rl.ffi.string(self._string_pointer).decode('utf-8') + self._clear() + return result def render(self, rect, title, sub_title): gui_label(rl.Rectangle(rect.x, rect.y, rect.width, 95), title, 90) gui_label(rl.Rectangle(rect.x, rect.y + 95, rect.width, 60), sub_title, 55, rl.GRAY) if gui_button(rl.Rectangle(rect.x + rect.width - 300, rect.y, 300, 100), "Cancel"): - return -1 + self._clear() + return 0 # Text box for input - rl.gui_text_box(rl.Rectangle(rect.x, rect.y + 160, rect.width, 100), self._input_text, self._max_text_size, True) - + self._sync_string_pointer() + rl.gui_text_box( + rl.Rectangle(rect.x, rect.y + 160, rect.width, 100), self._string_pointer, self._max_text_size, True + ) + self._input_text = rl.ffi.string(self._string_pointer).decode('utf-8') h_space, v_space = 15, 15 row_y_start = rect.y + 300 # Starting Y position for the first row key_height = (rect.height - 300 - 3 * v_space) / 4 @@ -77,7 +82,11 @@ class Keyboard: if i > 0: start_x += h_space - new_width = (key_width * 3 + h_space * 2) if key == SPACE_KEY else (key_width * 2 + h_space if key == ENTER_KEY else key_width) + new_width = ( + (key_width * 3 + h_space * 2) + if key == SPACE_KEY + else (key_width * 2 + h_space if key == ENTER_KEY else key_width) + ) key_rect = rl.Rectangle(start_x, row_y_start + row * (key_height + v_space), new_width, key_height) start_x += new_width @@ -87,7 +96,7 @@ class Keyboard: else: self.handle_key_press(key) - return 0 + return -1 def handle_key_press(self, key): if key in (SHIFT_DOWN_KEY, ABC_KEY): @@ -102,3 +111,14 @@ class Keyboard: self._input_text = self._input_text[:-1] elif key != BACKSPACE_KEY and len(self._input_text) < self._max_text_size: self._input_text += key + + def _clear(self): + self._input_text = '' + self._string_pointer[0] = b'\0' + + def _sync_string_pointer(self): + """Sync the C-string pointer with the internal Python string.""" + encoded = self._input_text.encode("utf-8")[:self._max_text_size - 1] # Leave room for null terminator + buffer = rl.ffi.buffer(self._string_pointer) + buffer[:len(encoded)] = encoded + self._string_pointer[len(encoded)] = b'\0' # Null terminate diff --git a/system/ui/widgets/network.py b/system/ui/widgets/network.py index 1aab7dceb6..3ee2806930 100644 --- a/system/ui/widgets/network.py +++ b/system/ui/widgets/network.py @@ -11,7 +11,7 @@ from openpilot.system.ui.widgets.keyboard import Keyboard from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog NM_DEVICE_STATE_NEED_AUTH = 60 - +ITEM_HEIGHT = 160 class ActionState(IntEnum): NONE = 0 @@ -28,7 +28,6 @@ class WifiManagerUI: self.wifi_manager = wifi_manager self.wifi_manager.need_auth_callback = self._need_auth self._selected_network = None - self.item_height = 160 self.btn_width = 200 self.current_action: ActionState = ActionState.NONE self.scroll_panel = GuiScrollPanel() @@ -57,51 +56,49 @@ class WifiManagerUI: if self.current_action == ActionState.NEED_AUTH: result = self.keyboard.render(rect, 'Enter password', f'for {self._selected_network.ssid}') - if result == 0: - return - elif result == 1: - self.current_action = ActionState.NONE + if result == 1: asyncio.create_task(self.connect_to_network(self.keyboard.text)) - else: + elif result == 0: self.current_action = ActionState.NONE return self._draw_network_list(rect) def _draw_network_list(self, rect: rl.Rectangle): - content_rect = rl.Rectangle(rect.x, rect.y, rect.width, len(self.wifi_manager.networks) * self.item_height) + content_rect = rl.Rectangle(rect.x, rect.y, rect.width, len(self.wifi_manager.networks) * ITEM_HEIGHT) offset = self.scroll_panel.handle_scroll(rect, content_rect) clicked = self.scroll_panel.is_click_valid() rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height)) for i, network in enumerate(self.wifi_manager.networks): - y_offset = i * self.item_height + offset.y - item_rect = rl.Rectangle(rect.x, y_offset, rect.width, self.item_height) + y_offset = rect.y + i * ITEM_HEIGHT + offset.y + item_rect = rl.Rectangle(rect.x, y_offset, rect.width, ITEM_HEIGHT) + if not rl.check_collision_recs(item_rect, rect): + continue - if rl.check_collision_recs(item_rect, rect): - self._draw_network_item(item_rect, network, clicked) - if i < len(self.wifi_manager.networks) - 1: - line_y = int(item_rect.y + item_rect.height - 1) - rl.draw_line(int(item_rect.x), int(line_y), int(item_rect.x + item_rect.width), line_y, rl.LIGHTGRAY) + self._draw_network_item(item_rect, network, clicked) + if i < len(self.wifi_manager.networks) - 1: + line_y = int(item_rect.y + item_rect.height - 1) + rl.draw_line(int(item_rect.x), int(line_y), int(item_rect.x + item_rect.width), line_y, rl.LIGHTGRAY) rl.end_scissor_mode() def _draw_network_item(self, rect, network: NetworkInfo, clicked: bool): - label_rect = rl.Rectangle(rect.x, rect.y, rect.width - self.btn_width * 2, self.item_height) - state_rect = rl.Rectangle(rect.x + rect.width - self.btn_width * 2 - 150, rect.y, 300, self.item_height) + label_rect = rl.Rectangle(rect.x, rect.y, rect.width - self.btn_width * 2, ITEM_HEIGHT) + state_rect = rl.Rectangle(rect.x + rect.width - self.btn_width * 2 - 150, rect.y, 300, ITEM_HEIGHT) gui_label(label_rect, network.ssid, 55) if network.is_connected and self.current_action == ActionState.NONE: rl.gui_label(state_rect, "Connected") - elif self.current_action == "Connecting" and self._selected_network and self._selected_network.ssid == network.ssid: + elif self.current_action == ActionState.CONNECTING and self._selected_network and self._selected_network.ssid == network.ssid: rl.gui_label(state_rect, "CONNECTING...") # If the network is saved, show the "Forget" button if self.wifi_manager.is_saved(network.ssid): forget_btn_rect = rl.Rectangle( rect.x + rect.width - self.btn_width, - rect.y + (self.item_height - 80) / 2, + rect.y + (ITEM_HEIGHT - 80) / 2, self.btn_width, 80, ) From ea71cb50d7b18cd668992d9293e9d864c99ab875 Mon Sep 17 00:00:00 2001 From: deanlee Date: Mon, 10 Mar 2025 23:28:16 +0800 Subject: [PATCH 08/16] remove duplicate --- system/ui/lib/wifi_manager.py | 34 +++++++++++++++++++++------------- system/ui/widgets/keyboard.py | 2 +- system/ui/widgets/network.py | 14 +++++++++----- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 44ac5e23fc..0e2634c7ff 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -19,6 +19,7 @@ NM_WIRELESS_IFACE = 'org.freedesktop.NetworkManager.Device.Wireless' NM_PROPERTIES_IFACE = 'org.freedesktop.DBus.Properties' NM_DEVICE_IFACE = "org.freedesktop.NetworkManager.Device" +NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT = 8 # NetworkManager device states class NMDeviceState(IntEnum): @@ -51,14 +52,15 @@ class NetworkInfo: class WifiManager: def __init__(self): self.networks: list[NetworkInfo] = [] - self.bus: MessageBus | None = None - self.device_path: str | None = None + self.bus: MessageBus = None + self.device_path: str = '' self.device_proxy = None self.saved_connections: dict[str, str] = {} self.active_ap_path: str = '' self.scan_task: asyncio.Task | None = None self.running: bool = True self.need_auth_callback = None + self.activated_callback = None async def connect(self) -> None: """Connect to the DBus system bus.""" @@ -96,7 +98,7 @@ class WifiManager: except DBusError as e: cloudlog.warning(f"Scan request failed: {str(e)}") - async def get_active_access_point(self) -> str: + async def get_active_access_point(self): try: props_iface = self.device_proxy.get_interface(NM_PROPERTIES_IFACE) ap_path = await props_iface.call_get(NM_WIRELESS_IFACE, 'ActiveAccessPoint') @@ -183,7 +185,7 @@ class WifiManager: device = await self.bus.introspect(NM, device_path) device_proxy = self.bus.get_proxy_object(NM, device_path, device) device_interface = device_proxy.get_interface(NM_DEVICE_IFACE) - device_type = await device_interface.get_device_type() + device_type = await device_interface.get_device_type() # type: ignore[attr-defined] if device_type == 2: # WiFi device self.device_path = device_path self.device_proxy = device_proxy @@ -232,11 +234,17 @@ class WifiManager: def _on_state_changed(self, new_state: int, old_state: int, reason: int): print(f"State changed: {old_state} -> {new_state}, reason: {reason}") if new_state == NMDeviceState.ACTIVATED: + if self.activated_callback: + self.activated_callback() asyncio.create_task(self._update_connection_status()) elif new_state in (NMDeviceState.DISCONNECTED, NMDeviceState.NEED_AUTH): for network in self.networks: network.is_connected = False - if new_state == NMDeviceState.NEED_AUTH and self.need_auth_callback: + if ( + new_state == NMDeviceState.NEED_AUTH + and reason == NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT + and self.need_auth_callback + ): self.need_auth_callback() def _on_new_connection(self, path: str) -> None: @@ -289,10 +297,9 @@ class WifiManager: async def _get_available_networks(self): """Get a list of available networks via NetworkManager.""" - networks = [] wifi_iface = self.device_proxy.get_interface(NM_WIRELESS_IFACE) access_points = await wifi_iface.get_access_points() - + network_dict = {} for ap_path in access_points: try: props_iface = await self._get_interface(NM, ap_path, NM_PROPERTIES_IFACE) @@ -303,27 +310,28 @@ class WifiManager: continue bssid = properties.get('HwAddress', Variant('s', '')).value + strength = properties['Strength'].value flags = properties['Flags'].value wpa_flags = properties['WpaFlags'].value rsn_flags = properties['RsnFlags'].value - - networks.append( - NetworkInfo( + existing_network = network_dict.get(ssid) + if not existing_network or ((not existing_network.bssid and bssid) or (existing_network.strength < strength)): + network_dict[ssid] = NetworkInfo( ssid=ssid, - strength=properties['Strength'].value, + strength=strength, security_type=self._get_security_type(flags, wpa_flags, rsn_flags), path=ap_path, bssid=bssid, is_connected=self.active_ap_path == ap_path, ) - ) + except DBusError as e: cloudlog.error(f"Error fetching networks: {e}") except Exception as e: cloudlog.error({e}) self.networks = sorted( - networks, + network_dict.values(), key=lambda network: ( not network.is_connected, -network.strength, # Higher signal strength first diff --git a/system/ui/widgets/keyboard.py b/system/ui/widgets/keyboard.py index 4d970b46dc..0c2fe65bd7 100644 --- a/system/ui/widgets/keyboard.py +++ b/system/ui/widgets/keyboard.py @@ -50,7 +50,7 @@ class Keyboard: self._clear() @property - def text(self) -> str: + def text(self): result = rl.ffi.string(self._string_pointer).decode('utf-8') self._clear() return result diff --git a/system/ui/widgets/network.py b/system/ui/widgets/network.py index 3ee2806930..a731ee56be 100644 --- a/system/ui/widgets/network.py +++ b/system/ui/widgets/network.py @@ -1,7 +1,6 @@ import asyncio import pyray as rl from enum import IntEnum -from dbus_next.constants import MessageType from openpilot.system.ui.lib.wifi_manager import WifiManager, NetworkInfo from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.button import gui_button @@ -26,7 +25,8 @@ class ActionState(IntEnum): class WifiManagerUI: def __init__(self, wifi_manager): self.wifi_manager = wifi_manager - self.wifi_manager.need_auth_callback = self._need_auth + self.wifi_manager.need_auth_callback = self._on_need_auth + self.wifi_manager.activated_callback = self._on_activated self._selected_network = None self.btn_width = 200 self.current_action: ActionState = ActionState.NONE @@ -128,10 +128,14 @@ class WifiManagerUI: await self.wifi_manager.activate_connection(self._selected_network.ssid) else: await self.wifi_manager.connect_to_network(self._selected_network.ssid, password) - self.current_action = ActionState.NONE - def _need_auth(self): - self.current_action = ActionState.NEED_AUTH + def _on_need_auth(self): + if self.current_action == ActionState.CONNECTING and self._selected_network: + self.current_action = ActionState.NEED_AUTH + + def _on_activated(self): + if self.current_action == ActionState.CONNECTING: + self.current_action = ActionState.NONE async def main(): From e50442aff068571bd68be77bc68a651e834232db Mon Sep 17 00:00:00 2001 From: Cameron Clough Date: Wed, 23 Apr 2025 14:13:50 +0100 Subject: [PATCH 09/16] typos --- system/ui/lib/wifi_manager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 0e2634c7ff..df75a6206b 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -135,7 +135,7 @@ class WifiManager: async def connect_to_network( self, ssid: str, password: str = None, bssid: str = None, is_hidden: bool = False ) -> None: - """Connect to a selected WiFi network.""" + """Connect to a selected Wi-Fi network.""" try: # settings_iface = await self._get_interface(NM, NM_SETTINGS_PATH, NM_SETTINGS_IFACE) connection = { @@ -186,7 +186,7 @@ class WifiManager: device_proxy = self.bus.get_proxy_object(NM, device_path, device) device_interface = device_proxy.get_interface(NM_DEVICE_IFACE) device_type = await device_interface.get_device_type() # type: ignore[attr-defined] - if device_type == 2: # WiFi device + if device_type == 2: # Wi-Fi device self.device_path = device_path self.device_proxy = device_proxy return True @@ -279,7 +279,7 @@ class WifiManager: await self._get_available_networks() async def _add_match_rule(self, rule): - """ "Add a match rule on the bus.""" + """Add a match rule on the bus.""" reply = await self.bus.call( Message( message_type=MessageType.METHOD_CALL, From 6b0ba3dee8fd41dbe2467be14f97146c0f4c083c Mon Sep 17 00:00:00 2001 From: Cameron Clough Date: Wed, 23 Apr 2025 15:41:25 +0100 Subject: [PATCH 10/16] use gui_app render --- system/ui/widgets/network.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/system/ui/widgets/network.py b/system/ui/widgets/network.py index a731ee56be..035514c7fb 100644 --- a/system/ui/widgets/network.py +++ b/system/ui/widgets/network.py @@ -144,14 +144,10 @@ async def main(): wifi_manager = WifiManager() wifi_ui = WifiManagerUI(wifi_manager) - while not rl.window_should_close(): - rl.begin_drawing() - rl.clear_background(rl.BLACK) - + for _ in gui_app.render(): wifi_ui.render(rl.Rectangle(50, 50, gui_app.width - 100, gui_app.height - 100)) - rl.end_drawing() - await asyncio.sleep(0.001) + gui_app.close() if __name__ == "__main__": From 44332eb186e9f283e0ffcc64e386646154186922 Mon Sep 17 00:00:00 2001 From: Cameron Clough Date: Wed, 23 Apr 2025 17:05:00 +0100 Subject: [PATCH 11/16] refactor --- system/ui/lib/wifi_manager.py | 126 ++++++++++++++++++++++---- system/ui/widgets/network.py | 165 ++++++++++++++++++---------------- 2 files changed, 199 insertions(+), 92 deletions(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index df75a6206b..1ec318485f 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -1,11 +1,15 @@ import asyncio +import threading +import time +import uuid +from collections.abc import Callable +from dataclasses import dataclass +from enum import IntEnum + from dbus_next.aio import MessageBus from dbus_next import BusType, Variant, Message from dbus_next.errors import DBusError from dbus_next.constants import MessageType -from enum import IntEnum -import uuid -from dataclasses import dataclass from openpilot.common.swaglog import cloudlog # NetworkManager constants @@ -21,6 +25,7 @@ NM_DEVICE_IFACE = "org.freedesktop.NetworkManager.Device" NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT = 8 + # NetworkManager device states class NMDeviceState(IntEnum): DISCONNECTED = 30 @@ -49,8 +54,16 @@ class NetworkInfo: # saved_path: str +@dataclass +class WifiManagerCallbacks: + need_auth: Callable[[], None] | None = None + activated: Callable[[], None] | None = None + forgotten: Callable[[], None] | None = None + + class WifiManager: - def __init__(self): + def __init__(self, callbacks: WifiManagerCallbacks): + self.callbacks = callbacks self.networks: list[NetworkInfo] = [] self.bus: MessageBus = None self.device_path: str = '' @@ -59,8 +72,6 @@ class WifiManager: self.active_ap_path: str = '' self.scan_task: asyncio.Task | None = None self.running: bool = True - self.need_auth_callback = None - self.activated_callback = None async def connect(self) -> None: """Connect to the DBus system bus.""" @@ -132,9 +143,7 @@ class WifiManager: cloudlog.error(f"Failed to activate connection {ssid}: {str(e)}") return False - async def connect_to_network( - self, ssid: str, password: str = None, bssid: str = None, is_hidden: bool = False - ) -> None: + async def connect_to_network(self, ssid: str, password: str = None, bssid: str = None, is_hidden: bool = False) -> None: """Connect to a selected Wi-Fi network.""" try: # settings_iface = await self._get_interface(NM, NM_SETTINGS_PATH, NM_SETTINGS_IFACE) @@ -234,18 +243,14 @@ class WifiManager: def _on_state_changed(self, new_state: int, old_state: int, reason: int): print(f"State changed: {old_state} -> {new_state}, reason: {reason}") if new_state == NMDeviceState.ACTIVATED: - if self.activated_callback: - self.activated_callback() + if self.callbacks.activated: + self.callbacks.activated() asyncio.create_task(self._update_connection_status()) elif new_state in (NMDeviceState.DISCONNECTED, NMDeviceState.NEED_AUTH): for network in self.networks: network.is_connected = False - if ( - new_state == NMDeviceState.NEED_AUTH - and reason == NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT - and self.need_auth_callback - ): - self.need_auth_callback() + if new_state == NMDeviceState.NEED_AUTH and reason == NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT and self.callbacks.need_auth: + self.callbacks.need_auth() def _on_new_connection(self, path: str) -> None: """Callback for NewConnection signal.""" @@ -389,3 +394,90 @@ class WifiManager: if wpa_flags: # WPA flags indicate WPA return SecurityType.WPA return SecurityType.UNSUPPORTED + + +class WifiManagerWrapper: + def __init__(self): + self._manager: WifiManager | None = None + self._callbacks: WifiManagerCallbacks = WifiManagerCallbacks() + + self._loop = None + self._running = False + self._lock = threading.RLock() + + self._thread = threading.Thread(target=self._run, daemon=True) + self._thread.start() + + while self._thread is not None and not self._running: + time.sleep(0.1) + + @property + def callbacks(self) -> WifiManagerCallbacks: + return self._callbacks + + @callbacks.setter + def callbacks(self, callbacks: WifiManagerCallbacks): + with self._lock: + self._callbacks = callbacks + + def _run(self): + self._loop = asyncio.new_event_loop() + asyncio.set_event_loop(self._loop) + + try: + self._manager = WifiManager(self._callbacks) + self._running = True + self._loop.run_forever() + except Exception as e: + cloudlog.error(f"Error in WifiManagerWrapper thread: {e}") + finally: + if self._loop.is_running(): + self._loop.stop() + self._running = False + + def shutdown(self): + if self._running: + self._run_coroutine(self._manager.shutdown()) + if self._loop and self._loop.is_running(): + self._loop.call_soon_threadsafe(self._loop.stop) + if self._thread and self._thread.is_alive(): + self._thread.join(timeout=2.0) + self._running = False + + @property + def networks(self) -> list[NetworkInfo]: + """Get the current list of networks (thread-safe).""" + with self._lock: + return self._manager.networks if self._manager else [] + + def is_saved(self, ssid: str) -> bool: + """Check if a network is saved (thread-safe).""" + with self._lock: + return self._manager.is_saved(ssid) if self._manager else False + + def connect(self): + """Connect to DBus and start Wi-Fi scanning.""" + self._run_coroutine(self._manager.connect()) + + def request_scan(self): + """Request a scan for Wi-Fi networks.""" + self._run_coroutine(self._manager.request_scan()) + + def forget_connection(self, ssid: str): + """Forget a saved Wi-Fi connection.""" + self._run_coroutine(self._manager.forget_connection(ssid)) + + def activate_connection(self, ssid: str): + """Activate an existing Wi-Fi connection.""" + self._run_coroutine(self._manager.activate_connection(ssid)) + + def connect_to_network(self, ssid: str, password: str = None, bssid: str = None, is_hidden: bool = False): + """Connect to a Wi-Fi network.""" + self._run_coroutine(self._manager.connect_to_network(ssid, password, bssid, is_hidden)) + + def _run_coroutine(self, coro): + """Run a coroutine in the async thread.""" + if not self._running or not self._loop: + cloudlog.error("WifiManager thread is not running") + return + asyncio.run_coroutine_threadsafe(coro, self._loop) diff --git a/system/ui/widgets/network.py b/system/ui/widgets/network.py index 035514c7fb..e09d7e6f3b 100644 --- a/system/ui/widgets/network.py +++ b/system/ui/widgets/network.py @@ -1,7 +1,8 @@ -import asyncio +from dataclasses import dataclass +from typing import Literal + import pyray as rl -from enum import IntEnum -from openpilot.system.ui.lib.wifi_manager import WifiManager, NetworkInfo +from openpilot.system.ui.lib.wifi_manager import NetworkInfo, WifiManagerCallbacks, WifiManagerWrapper from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.button import gui_button from openpilot.system.ui.lib.label import gui_label @@ -12,57 +13,67 @@ from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog NM_DEVICE_STATE_NEED_AUTH = 60 ITEM_HEIGHT = 160 -class ActionState(IntEnum): - NONE = 0 - CONNECT = 1 - CONNECTING = 2 - FORGOT = 3 - FORGETTING = 4 - NEED_AUTH = 5 - SHOW_FORGOT_CONFIRM = 6 + +@dataclass +class StateIdle: + action: Literal["idle"] = "idle" + +@dataclass +class StateConnecting: + network: NetworkInfo + action: Literal["connecting"] = "connecting" + +@dataclass +class StateNeedsAuth: + network: NetworkInfo + action: Literal["needs_auth"] = "needs_auth" + +@dataclass +class StateShowForgetConfirm: + network: NetworkInfo + action: Literal["show_forget_confirm"] = "show_forget_confirm" + +@dataclass +class StateForgetting: + network: NetworkInfo + action: Literal["forgetting"] = "forgetting" + +UIState = StateIdle | StateConnecting | StateNeedsAuth | StateShowForgetConfirm | StateForgetting class WifiManagerUI: - def __init__(self, wifi_manager): - self.wifi_manager = wifi_manager - self.wifi_manager.need_auth_callback = self._on_need_auth - self.wifi_manager.activated_callback = self._on_activated - self._selected_network = None + def __init__(self, wifi_manager: WifiManagerWrapper): + self.state: UIState = StateIdle() self.btn_width = 200 - self.current_action: ActionState = ActionState.NONE self.scroll_panel = GuiScrollPanel() self.keyboard = Keyboard() - asyncio.create_task(self._initialize()) - - async def _initialize(self) -> None: - try: - await self.wifi_manager.connect() - except Exception as e: - print(f"Initialization error: {e}") + self.wifi_manager = wifi_manager + self.wifi_manager.callbacks = WifiManagerCallbacks(self._on_need_auth, self._on_activated, self._on_forgotten) + self.wifi_manager.connect() def render(self, rect: rl.Rectangle): if not self.wifi_manager.networks: - gui_label(rect, "Scanning Wi-Fi networks...", 40, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) + gui_label(rect, "Scanning Wi-Fi networks...", 72, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) return - if self.current_action == ActionState.SHOW_FORGOT_CONFIRM: - result = confirm_dialog(rect, f'Forget Wi-Fi Network "{self._selected_network.ssid}"?', 'Forget') - if result == 1: - asyncio.create_task(self.forgot_network()) - elif result == 0: - self.current_action = ActionState.NONE - return + match self.state: + case StateNeedsAuth(network): + result = self.keyboard.render(rect, "Enter password", f"for {network.ssid}") + if result == 1: + self.connect_to_network(network, self.keyboard.text) + elif result == 0: + self.state = StateIdle() - if self.current_action == ActionState.NEED_AUTH: - result = self.keyboard.render(rect, 'Enter password', f'for {self._selected_network.ssid}') - if result == 1: - asyncio.create_task(self.connect_to_network(self.keyboard.text)) - elif result == 0: - self.current_action = ActionState.NONE - return + case StateShowForgetConfirm(network): + result = confirm_dialog(rect, f'Forget Wi-Fi Network "{network.ssid}"?', "Forget") + if result == 1: + self.forget_network(network) + elif result == 0: + self.state = StateIdle() - self._draw_network_list(rect) + case _: + self._draw_network_list(rect) def _draw_network_list(self, rect: rl.Rectangle): content_rect = rl.Rectangle(rect.x, rect.y, rect.width, len(self.wifi_manager.networks) * ITEM_HEIGHT) @@ -89,10 +100,18 @@ class WifiManagerUI: gui_label(label_rect, network.ssid, 55) - if network.is_connected and self.current_action == ActionState.NONE: - rl.gui_label(state_rect, "Connected") - elif self.current_action == ActionState.CONNECTING and self._selected_network and self._selected_network.ssid == network.ssid: - rl.gui_label(state_rect, "CONNECTING...") + status_text = "" + if network.is_connected: + status_text = "Connected" + match self.state: + case StateConnecting(network=connecting): + if connecting.ssid == network.ssid: + status_text = "CONNECTING..." + case StateForgetting(network=forgetting): + if forgetting.ssid == network.ssid: + status_text = "FORGETTING..." + if status_text: + rl.gui_label(state_rect, status_text) # If the network is saved, show the "Forget" button if self.wifi_manager.is_saved(network.ssid): @@ -102,46 +121,42 @@ class WifiManagerUI: self.btn_width, 80, ) - if gui_button(forget_btn_rect, "Forget") and self.current_action == ActionState.NONE and clicked: - self._selected_network = network - self.current_action = ActionState.SHOW_FORGOT_CONFIRM - - if ( - self.current_action == ActionState.NONE - and rl.check_collision_point_rec(rl.get_mouse_position(), label_rect) - and clicked - ): - self._selected_network = network - if not self.wifi_manager.is_saved(self._selected_network.ssid): - self.current_action = ActionState.NEED_AUTH - else: - asyncio.create_task(self.connect_to_network()) + if isinstance(self.state, StateIdle) and gui_button(forget_btn_rect, "Forget") and clicked: + self.state = StateShowForgetConfirm(network) - async def forgot_network(self): - self.current_action = ActionState.FORGETTING - await self.wifi_manager.forget_connection(self._selected_network.ssid) - self.current_action = ActionState.NONE + if isinstance(self.state, StateIdle) and rl.check_collision_point_rec(rl.get_mouse_position(), label_rect) and clicked: + if not self.wifi_manager.is_saved(network.ssid): + self.state = StateNeedsAuth(network) + else: + self.connect_to_network(network) - async def connect_to_network(self, password=''): - self.current_action = ActionState.CONNECTING - if self.wifi_manager.is_saved(self._selected_network.ssid) and not password: - await self.wifi_manager.activate_connection(self._selected_network.ssid) + def connect_to_network(self, network: NetworkInfo, password=''): + if self.wifi_manager.is_saved(network.ssid) and not password: + self.wifi_manager.activate_connection(network.ssid) else: - await self.wifi_manager.connect_to_network(self._selected_network.ssid, password) + self.wifi_manager.connect_to_network(network.ssid, password) + + def forget_network(self, network: NetworkInfo): + self.state = StateForgetting(network) + self.wifi_manager.forget_connection(network.ssid) def _on_need_auth(self): - if self.current_action == ActionState.CONNECTING and self._selected_network: - self.current_action = ActionState.NEED_AUTH + match self.state: + case StateConnecting(network): + self.state = StateNeedsAuth(network) def _on_activated(self): - if self.current_action == ActionState.CONNECTING: - self.current_action = ActionState.NONE + if isinstance(self.state, StateConnecting): + self.state = StateIdle() + def _on_forgotten(self): + if isinstance(self.state, StateForgetting): + self.state = StateIdle() -async def main(): - gui_app.init_window("Wifi Manager") - wifi_manager = WifiManager() +def main(): + gui_app.init_window("Wi-Fi Manager") + wifi_manager = WifiManagerWrapper() wifi_ui = WifiManagerUI(wifi_manager) for _ in gui_app.render(): @@ -151,4 +166,4 @@ async def main(): if __name__ == "__main__": - asyncio.run(main()) + main() From eaa5a881511e13bb97b8d32e9ea97e5f94d49f39 Mon Sep 17 00:00:00 2001 From: Cameron Clough Date: Wed, 23 Apr 2025 17:07:51 +0100 Subject: [PATCH 12/16] cleanup --- system/ui/lib/wifi_manager.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 1ec318485f..b2f55f0144 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -25,7 +25,6 @@ NM_DEVICE_IFACE = "org.freedesktop.NetworkManager.Device" NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT = 8 - # NetworkManager device states class NMDeviceState(IntEnum): DISCONNECTED = 30 @@ -34,7 +33,6 @@ class NMDeviceState(IntEnum): IP_CONFIG = 70 ACTIVATED = 100 - class SecurityType(IntEnum): OPEN = 0 WPA = 1 @@ -42,7 +40,6 @@ class SecurityType(IntEnum): WPA3 = 3 UNSUPPORTED = 4 - @dataclass class NetworkInfo: ssid: str @@ -66,10 +63,10 @@ class WifiManager: self.callbacks = callbacks self.networks: list[NetworkInfo] = [] self.bus: MessageBus = None - self.device_path: str = '' + self.device_path: str = "" self.device_proxy = None self.saved_connections: dict[str, str] = {} - self.active_ap_path: str = '' + self.active_ap_path: str = "" self.scan_task: asyncio.Task | None = None self.running: bool = True @@ -137,7 +134,7 @@ class WifiManager: return False try: nm_iface = await self._get_interface(NM, NM_PATH, NM_IFACE) - await nm_iface.call_activate_connection(connection_path, self.device_path, '/') + await nm_iface.call_activate_connection(connection_path, self.device_path, "/") return True except DBusError as e: cloudlog.error(f"Failed to activate connection {ssid}: {str(e)}") From c4edd9fc042afa490f39edfcf05b1aeb6d552c7f Mon Sep 17 00:00:00 2001 From: Cameron Clough Date: Wed, 23 Apr 2025 17:13:36 +0100 Subject: [PATCH 13/16] cleanup --- system/ui/lib/wifi_manager.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index b2f55f0144..5560bd79d2 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -143,7 +143,6 @@ class WifiManager: async def connect_to_network(self, ssid: str, password: str = None, bssid: str = None, is_hidden: bool = False) -> None: """Connect to a selected Wi-Fi network.""" try: - # settings_iface = await self._get_interface(NM, NM_SETTINGS_PATH, NM_SETTINGS_IFACE) connection = { 'connection': { 'type': Variant('s', '802-11-wireless'), @@ -172,9 +171,6 @@ class WifiManager: nm_iface = await self._get_interface(NM, NM_PATH, NM_IFACE) await nm_iface.call_add_and_activate_connection(connection, self.device_path, "/") - - # for network in self.networks: - # network.is_connected = True if network.ssid == ssid else False await self._update_connection_status() except DBusError as e: From ce1b9e30d327ab35e2c016345a470da6aeb1c765 Mon Sep 17 00:00:00 2001 From: Cameron Clough Date: Wed, 23 Apr 2025 17:13:41 +0100 Subject: [PATCH 14/16] shutdown --- system/ui/widgets/network.py | 1 + 1 file changed, 1 insertion(+) diff --git a/system/ui/widgets/network.py b/system/ui/widgets/network.py index e09d7e6f3b..f3fe2fe225 100644 --- a/system/ui/widgets/network.py +++ b/system/ui/widgets/network.py @@ -162,6 +162,7 @@ def main(): for _ in gui_app.render(): wifi_ui.render(rl.Rectangle(50, 50, gui_app.width - 100, gui_app.height - 100)) + wifi_manager.shutdown() gui_app.close() From 71201455987c76987affa41183ef8ac4459ab4d9 Mon Sep 17 00:00:00 2001 From: Cameron Clough Date: Wed, 23 Apr 2025 18:54:24 +0100 Subject: [PATCH 15/16] fix types --- system/ui/lib/wifi_manager.py | 53 ++++++++++++++++++----------------- system/ui/widgets/network.py | 3 +- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 5560bd79d2..c6aeaccf70 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -59,8 +59,8 @@ class WifiManagerCallbacks: class WifiManager: - def __init__(self, callbacks: WifiManagerCallbacks): - self.callbacks = callbacks + def __init__(self, callbacks): + self.callbacks: WifiManagerCallbacks = callbacks self.networks: list[NetworkInfo] = [] self.bus: MessageBus = None self.device_path: str = "" @@ -394,24 +394,18 @@ class WifiManagerWrapper: self._manager: WifiManager | None = None self._callbacks: WifiManagerCallbacks = WifiManagerCallbacks() - self._loop = None - self._running = False - self._lock = threading.RLock() - self._thread = threading.Thread(target=self._run, daemon=True) - self._thread.start() - - while self._thread is not None and not self._running: - time.sleep(0.1) + self._loop: asyncio.EventLoop | None = None + self._running = False - @property - def callbacks(self) -> WifiManagerCallbacks: - return self._callbacks + def set_callbacks(self, callbacks: WifiManagerCallbacks): + self._callbacks = callbacks - @callbacks.setter - def callbacks(self, callbacks: WifiManagerCallbacks): - with self._lock: - self._callbacks = callbacks + def start(self) -> None: + if not self._running: + self._thread.start() + while self._thread is not None and not self._running: + time.sleep(0.1) def _run(self): self._loop = asyncio.new_event_loop() @@ -428,9 +422,10 @@ class WifiManagerWrapper: self._loop.stop() self._running = False - def shutdown(self): + def shutdown(self) -> None: if self._running: - self._run_coroutine(self._manager.shutdown()) + if self._manager is not None: + self._run_coroutine(self._manager.shutdown()) if self._loop and self._loop.is_running(): self._loop.call_soon_threadsafe(self._loop.stop) if self._thread and self._thread.is_alive(): @@ -439,33 +434,41 @@ class WifiManagerWrapper: @property def networks(self) -> list[NetworkInfo]: - """Get the current list of networks (thread-safe).""" - with self._lock: - return self._manager.networks if self._manager else [] + """Get the current list of networks.""" + return self._manager.networks if self._manager else [] def is_saved(self, ssid: str) -> bool: - """Check if a network is saved (thread-safe).""" - with self._lock: - return self._manager.is_saved(ssid) if self._manager else False + """Check if a network is saved.""" + return self._manager.is_saved(ssid) if self._manager else False def connect(self): """Connect to DBus and start Wi-Fi scanning.""" + if not self._manager: + return self._run_coroutine(self._manager.connect()) def request_scan(self): """Request a scan for Wi-Fi networks.""" + if not self._manager: + return self._run_coroutine(self._manager.request_scan()) def forget_connection(self, ssid: str): """Forget a saved Wi-Fi connection.""" + if not self._manager: + return self._run_coroutine(self._manager.forget_connection(ssid)) def activate_connection(self, ssid: str): """Activate an existing Wi-Fi connection.""" + if not self._manager: + return self._run_coroutine(self._manager.activate_connection(ssid)) def connect_to_network(self, ssid: str, password: str = None, bssid: str = None, is_hidden: bool = False): """Connect to a Wi-Fi network.""" + if not self._manager: + return self._run_coroutine(self._manager.connect_to_network(ssid, password, bssid, is_hidden)) def _run_coroutine(self, coro): diff --git a/system/ui/widgets/network.py b/system/ui/widgets/network.py index f3fe2fe225..47318e4a3a 100644 --- a/system/ui/widgets/network.py +++ b/system/ui/widgets/network.py @@ -49,7 +49,8 @@ class WifiManagerUI: self.keyboard = Keyboard() self.wifi_manager = wifi_manager - self.wifi_manager.callbacks = WifiManagerCallbacks(self._on_need_auth, self._on_activated, self._on_forgotten) + self.wifi_manager.set_callbacks(WifiManagerCallbacks(self._on_need_auth, self._on_activated, self._on_forgotten)) + self.wifi_manager.start() self.wifi_manager.connect() def render(self, rect: rl.Rectangle): From 63e4fec9c7ad3c44d36a6a9f24fd37ae00bd3519 Mon Sep 17 00:00:00 2001 From: Cameron Clough Date: Wed, 23 Apr 2025 19:00:46 +0100 Subject: [PATCH 16/16] revert --- system/ui/lib/scroll_panel.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/system/ui/lib/scroll_panel.py b/system/ui/lib/scroll_panel.py index bd54f739ab..ca8f427830 100644 --- a/system/ui/lib/scroll_panel.py +++ b/system/ui/lib/scroll_panel.py @@ -28,15 +28,11 @@ class GuiScrollPanel: mouse_pos = rl.get_mouse_position() # Handle dragging logic - if rl.check_collision_point_rec(mouse_pos, bounds) and rl.is_mouse_button_pressed( - rl.MouseButton.MOUSE_BUTTON_LEFT - ): + if rl.check_collision_point_rec(mouse_pos, bounds) and rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT): if self._scroll_state == ScrollState.IDLE: self._scroll_state = ScrollState.DRAGGING_CONTENT if self._show_vertical_scroll_bar: - scrollbar_width = rl.gui_get_style( - rl.GuiControl.LISTVIEW, rl.GuiListViewProperty.SCROLLBAR_WIDTH - ) + scrollbar_width = rl.gui_get_style(rl.GuiControl.LISTVIEW, rl.GuiListViewProperty.SCROLLBAR_WIDTH) scrollbar_x = bounds.x + bounds.width - scrollbar_width if mouse_pos.x >= scrollbar_x: self._scroll_state = ScrollState.DRAGGING_SCROLLBAR