From 73cba70d5591b713a02979b8cfc72618f0a89c7d Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Fri, 19 Dec 2025 14:39:02 +0100 Subject: [PATCH] WiFi app: delegate to WiFiService where possible --- .../com.micropythonos.wifi/assets/wifi.py | 223 +++++++----------- .../lib/mpos/net/wifi_service.py | 126 +++++++++- 2 files changed, 204 insertions(+), 145 deletions(-) diff --git a/internal_filesystem/builtin/apps/com.micropythonos.wifi/assets/wifi.py b/internal_filesystem/builtin/apps/com.micropythonos.wifi/assets/wifi.py index abb0e519..f1ab4469 100644 --- a/internal_filesystem/builtin/apps/com.micropythonos.wifi/assets/wifi.py +++ b/internal_filesystem/builtin/apps/com.micropythonos.wifi/assets/wifi.py @@ -1,4 +1,3 @@ -import os import time import lvgl as lv import _thread @@ -6,25 +5,24 @@ import _thread from mpos.apps import Activity, Intent from mpos.ui.keyboard import MposKeyboard -import mpos.config +import mpos.apps from mpos.net.wifi_service import WifiService -class WiFi(Activity): - prefs = None - saved_access_points={} +class WiFi(Activity): + """ + WiFi settings app for MicroPythonOS. + + This is a pure UI layer - all WiFi operations are delegated to WifiService. + """ + last_tried_ssid = "" last_tried_result = "" - have_network = True - try: - import network - except Exception as e: - have_network = False scan_button_scan_text = "Rescan" scan_button_scanning_text = "Scanning..." - scanned_ssids=[] + scanned_ssids = [] busy_scanning = False busy_connecting = False error_timer = None @@ -39,25 +37,25 @@ class WiFi(Activity): print("wifi.py onCreate") main_screen = lv.obj() main_screen.set_style_pad_all(15, 0) - self.aplist=lv.list(main_screen) - self.aplist.set_size(lv.pct(100),lv.pct(75)) - self.aplist.align(lv.ALIGN.TOP_MID,0,0) - self.error_label=lv.label(main_screen) + self.aplist = lv.list(main_screen) + self.aplist.set_size(lv.pct(100), lv.pct(75)) + self.aplist.align(lv.ALIGN.TOP_MID, 0, 0) + self.error_label = lv.label(main_screen) self.error_label.set_text("THIS IS ERROR TEXT THAT WILL BE SET LATER") - self.error_label.align_to(self.aplist, lv.ALIGN.OUT_BOTTOM_MID,0,0) + self.error_label.align_to(self.aplist, lv.ALIGN.OUT_BOTTOM_MID, 0, 0) self.error_label.add_flag(lv.obj.FLAG.HIDDEN) - self.add_network_button=lv.button(main_screen) - self.add_network_button.set_size(lv.SIZE_CONTENT,lv.pct(15)) - self.add_network_button.align(lv.ALIGN.BOTTOM_LEFT,0,0) - self.add_network_button.add_event_cb(self.add_network_callback,lv.EVENT.CLICKED,None) - self.add_network_button_label=lv.label(self.add_network_button) + self.add_network_button = lv.button(main_screen) + self.add_network_button.set_size(lv.SIZE_CONTENT, lv.pct(15)) + self.add_network_button.align(lv.ALIGN.BOTTOM_LEFT, 0, 0) + self.add_network_button.add_event_cb(self.add_network_callback, lv.EVENT.CLICKED, None) + self.add_network_button_label = lv.label(self.add_network_button) self.add_network_button_label.set_text("Add network") self.add_network_button_label.center() - self.scan_button=lv.button(main_screen) - self.scan_button.set_size(lv.SIZE_CONTENT,lv.pct(15)) - self.scan_button.align(lv.ALIGN.BOTTOM_RIGHT,0,0) - self.scan_button.add_event_cb(self.scan_cb,lv.EVENT.CLICKED,None) - self.scan_button_label=lv.label(self.scan_button) + self.scan_button = lv.button(main_screen) + self.scan_button.set_size(lv.SIZE_CONTENT, lv.pct(15)) + self.scan_button.align(lv.ALIGN.BOTTOM_RIGHT, 0, 0) + self.scan_button.add_event_cb(self.scan_cb, lv.EVENT.CLICKED, None) + self.scan_button_label = lv.label(self.scan_button) self.scan_button_label.set_text(self.scan_button_scan_text) self.scan_button_label.center() self.setContentView(main_screen) @@ -66,11 +64,9 @@ class WiFi(Activity): print("wifi.py onResume") super().onResume(screen) - if not self.prefs: - self.prefs = mpos.config.SharedPreferences("com.micropythonos.system.wifiservice") + # Ensure WifiService has loaded saved networks + WifiService.get_saved_networks() - self.saved_access_points = self.prefs.get_dict("access_points") - print(f"loaded access points from preferences: {self.saved_access_points}") if len(self.scanned_ssids) == 0: if WifiService.wifi_busy == False: WifiService.wifi_busy = True @@ -83,26 +79,16 @@ class WiFi(Activity): print(f"show_error: Displaying error: {message}") self.update_ui_threadsafe_if_foreground(self.error_label.set_text, message) self.update_ui_threadsafe_if_foreground(self.error_label.remove_flag, lv.obj.FLAG.HIDDEN) - self.error_timer = lv.timer_create(self.hide_error,5000,None) + self.error_timer = lv.timer_create(self.hide_error, 5000, None) self.error_timer.set_repeat_count(1) def hide_error(self, timer): - self.update_ui_threadsafe_if_foreground(self.error_label.add_flag,lv.obj.FLAG.HIDDEN) + self.update_ui_threadsafe_if_foreground(self.error_label.add_flag, lv.obj.FLAG.HIDDEN) def scan_networks_thread(self): print("scan_networks: Scanning for Wi-Fi networks") - if self.have_network: - wlan=network.WLAN(network.STA_IF) - if not wlan.isconnected(): # restart WiFi hardware in case it's in a bad state - wlan.active(False) - wlan.active(True) try: - if self.have_network: - networks = wlan.scan() - self.scanned_ssids = list(set(n[0].decode() for n in networks)) - else: - time.sleep(1) - self.scanned_ssids = ["Home WiFi", "Pretty Fly for a Wi Fi", "Winternet is coming", "The Promised LAN"] + self.scanned_ssids = WifiService.scan_networks() print(f"scan_networks: Found networks: {self.scanned_ssids}") except Exception as e: print(f"scan_networks: Scan failed: {e}") @@ -110,7 +96,7 @@ class WiFi(Activity): # scan done: self.busy_scanning = False WifiService.wifi_busy = False - self.update_ui_threadsafe_if_foreground(self.scan_button_label.set_text,self.scan_button_scan_text) + self.update_ui_threadsafe_if_foreground(self.scan_button_label.set_text, self.scan_button_scan_text) self.update_ui_threadsafe_if_foreground(self.scan_button.remove_state, lv.STATE.DISABLED) self.update_ui_threadsafe_if_foreground(self.refresh_list) @@ -126,28 +112,35 @@ class WiFi(Activity): def refresh_list(self): print("refresh_list: Clearing current list") - self.aplist.clean() # this causes an issue with lost taps if an ssid is clicked that has been removed + self.aplist.clean() # this causes an issue with lost taps if an ssid is clicked that has been removed print("refresh_list: Populating list with scanned networks") - for ssid in set(self.scanned_ssids + list(ssid for ssid in self.saved_access_points)): + + # Combine scanned SSIDs with saved networks + saved_networks = WifiService.get_saved_networks() + all_ssids = set(self.scanned_ssids + saved_networks) + + for ssid in all_ssids: if len(ssid) < 1 or len(ssid) > 32: print(f"Skipping too short or long SSID: {ssid}") continue print(f"refresh_list: Adding SSID: {ssid}") - button=self.aplist.add_button(None,ssid) - button.add_event_cb(lambda e, s=ssid: self.select_ssid_cb(s),lv.EVENT.CLICKED,None) + button = self.aplist.add_button(None, ssid) + button.add_event_cb(lambda e, s=ssid: self.select_ssid_cb(s), lv.EVENT.CLICKED, None) + + # Determine status status = "" - if self.have_network: - wlan=network.WLAN(network.STA_IF) - if wlan.isconnected() and wlan.config('essid')==ssid: - status="connected" - if status != "connected": - if self.last_tried_ssid == ssid: # implies not connected because not wlan.isconnected() - status = self.last_tried_result - elif ssid in self.saved_access_points: - status="saved" - label=lv.label(button) + current_ssid = WifiService.get_current_ssid() + if current_ssid == ssid: + status = "connected" + elif self.last_tried_ssid == ssid: + # Show last connection attempt result + status = self.last_tried_result + elif ssid in saved_networks: + status = "saved" + + label = lv.label(button) label.set_text(status) - label.align(lv.ALIGN.RIGHT_MID,0,0) + label.align(lv.ALIGN.RIGHT_MID, 0, 0) def add_network_callback(self, event): print(f"add_network_callback clicked") @@ -159,36 +152,28 @@ class WiFi(Activity): print("scan_cb: Scan button clicked, refreshing list") self.start_scan_networks() - def select_ssid_cb(self,ssid): + def select_ssid_cb(self, ssid): print(f"select_ssid_cb: SSID selected: {ssid}") intent = Intent(activity_class=EditNetwork) intent.putExtra("selected_ssid", ssid) - intent.putExtra("known_password", self.findSavedPassword(ssid)) + intent.putExtra("known_password", WifiService.get_network_password(ssid)) self.startActivityForResult(intent, self.edit_network_result_callback) - + def edit_network_result_callback(self, result): print(f"EditNetwork finished, result: {result}") if result.get("result_code") is True: data = result.get("data") if data: ssid = data.get("ssid") - editor = self.prefs.edit() forget = data.get("forget") if forget: - try: - del self.saved_access_points[ssid] - editor.put_dict("access_points", self.saved_access_points) - editor.commit() - self.refresh_list() - except Exception as e: - print(f"WARNING: could not forget access point, maybe it wasn't remembered in the first place: {e}") - else: # save or update + WifiService.forget_network(ssid) + self.refresh_list() + else: + # Save or update the network password = data.get("password") hidden = data.get("hidden") - self.setPassword(ssid, password, hidden) - editor.put_dict("access_points", self.saved_access_points) - editor.commit() - print(f"access points: {self.saved_access_points}") + WifiService.save_network(ssid, password, hidden) self.start_attempt_connecting(ssid, password) def start_attempt_connecting(self, ssid, password): @@ -200,58 +185,32 @@ class WiFi(Activity): else: self.busy_connecting = True _thread.stack_size(mpos.apps.good_stack_size()) - _thread.start_new_thread(self.attempt_connecting_thread, (ssid,password)) + _thread.start_new_thread(self.attempt_connecting_thread, (ssid, password)) def attempt_connecting_thread(self, ssid, password): - print(f"attempt_connecting_thread: Attempting to connect to SSID '{ssid}' with password '{password}'") - result="connected" + print(f"attempt_connecting_thread: Attempting to connect to SSID '{ssid}'") + result = "connected" try: - if self.have_network: - wlan=network.WLAN(network.STA_IF) - wlan.disconnect() - wlan.connect(ssid,password) - for i in range(10): - if wlan.isconnected(): - print(f"attempt_connecting: Connected to {ssid} after {i+1} seconds") - break - print(f"attempt_connecting: Waiting for connection, attempt {i+1}/10") - time.sleep(1) - if not wlan.isconnected(): - result="timeout" + if WifiService.attempt_connecting(ssid, password): + result = "connected" else: - print("Warning: not trying to connect because not self.have_network, just waiting a bit...") - time.sleep(5) + result = "timeout" except Exception as e: print(f"attempt_connecting: Connection error: {e}") - result=f"{e}" - self.show_error("Connecting to {ssid} failed!") + result = f"{e}" + self.show_error(f"Connecting to {ssid} failed!") + print(f"Connecting to {ssid} got result: {result}") self.last_tried_ssid = ssid self.last_tried_result = result - # also do a time sync, otherwise some apps (Nostr Wallet Connect) won't work: - if self.have_network and wlan.isconnected(): - mpos.time.sync_time() - self.busy_connecting=False + + # Note: Time sync is handled by WifiService.attempt_connecting() + + self.busy_connecting = False self.update_ui_threadsafe_if_foreground(self.scan_button_label.set_text, self.scan_button_scan_text) self.update_ui_threadsafe_if_foreground(self.scan_button.remove_state, lv.STATE.DISABLED) self.update_ui_threadsafe_if_foreground(self.refresh_list) - def findSavedPassword(self, ssid): - ap = self.saved_access_points.get(ssid) - if ap: - return ap.get("password") - return None - - def setPassword(self, ssid, password, hidden=False): - ap = self.saved_access_points.get(ssid) - if ap: - ap["password"] = password - if hidden is True: - ap["hidden"] = True - return - # if not found, then add it: - self.saved_access_points[ssid] = { "password": password, "hidden": hidden } - class EditNetwork(Activity): @@ -259,14 +218,14 @@ class EditNetwork(Activity): # Widgets: ssid_ta = None - password_ta=None + password_ta = None hidden_cb = None - keyboard=None - connect_button=None - cancel_button=None + keyboard = None + connect_button = None + cancel_button = None def onCreate(self): - password_page=lv.obj() + password_page = lv.obj() password_page.set_style_pad_all(0, lv.PART.MAIN) password_page.set_flex_flow(lv.FLEX_FLOW.COLUMN) self.selected_ssid = self.getIntent().extras.get("selected_ssid") @@ -275,31 +234,31 @@ class EditNetwork(Activity): # SSID: if self.selected_ssid is None: print("No ssid selected, the user should fill it out.") - label=lv.label(password_page) + label = lv.label(password_page) label.set_text(f"Network name:") - self.ssid_ta=lv.textarea(password_page) + self.ssid_ta = lv.textarea(password_page) self.ssid_ta.set_width(lv.pct(90)) self.ssid_ta.set_style_margin_left(5, lv.PART.MAIN) self.ssid_ta.set_one_line(True) self.ssid_ta.set_placeholder_text("Enter the SSID") - self.keyboard=MposKeyboard(password_page) + self.keyboard = MposKeyboard(password_page) self.keyboard.set_textarea(self.ssid_ta) self.keyboard.add_flag(lv.obj.FLAG.HIDDEN) - + # Password: - label=lv.label(password_page) + label = lv.label(password_page) if self.selected_ssid is None: label.set_text("Password:") else: label.set_text(f"Password for '{self.selected_ssid}':") - self.password_ta=lv.textarea(password_page) + self.password_ta = lv.textarea(password_page) self.password_ta.set_width(lv.pct(90)) self.password_ta.set_style_margin_left(5, lv.PART.MAIN) self.password_ta.set_one_line(True) if known_password: self.password_ta.set_text(known_password) self.password_ta.set_placeholder_text("Password") - self.keyboard=MposKeyboard(password_page) + self.keyboard = MposKeyboard(password_page) self.keyboard.set_textarea(self.password_ta) self.keyboard.add_flag(lv.obj.FLAG.HIDDEN) @@ -316,24 +275,24 @@ class EditNetwork(Activity): buttons.set_style_border_width(0, lv.PART.MAIN) # Delete button if self.selected_ssid: - self.forget_button=lv.button(buttons) + self.forget_button = lv.button(buttons) self.forget_button.align(lv.ALIGN.LEFT_MID, 0, 0) self.forget_button.add_event_cb(self.forget_cb, lv.EVENT.CLICKED, None) - label=lv.label(self.forget_button) + label = lv.label(self.forget_button) label.set_text("Forget") label.center() # Close button - self.cancel_button=lv.button(buttons) + self.cancel_button = lv.button(buttons) self.cancel_button.center() self.cancel_button.add_event_cb(lambda *args: self.finish(), lv.EVENT.CLICKED, None) - label=lv.label(self.cancel_button) + label = lv.label(self.cancel_button) label.set_text("Close") label.center() # Connect button self.connect_button = lv.button(buttons) self.connect_button.align(lv.ALIGN.RIGHT_MID, 0, 0) - self.connect_button.add_event_cb(self.connect_cb,lv.EVENT.CLICKED,None) - label=lv.label(self.connect_button) + self.connect_button.add_event_cb(self.connect_cb, lv.EVENT.CLICKED, None) + label = lv.label(self.connect_button) label.set_text("Connect") label.center() diff --git a/internal_filesystem/lib/mpos/net/wifi_service.py b/internal_filesystem/lib/mpos/net/wifi_service.py index 25d777a7..279d0dac 100644 --- a/internal_filesystem/lib/mpos/net/wifi_service.py +++ b/internal_filesystem/lib/mpos/net/wifi_service.py @@ -42,6 +42,9 @@ class WifiService: # Dictionary of saved access points {ssid: {password: "..."}} access_points = {} + # Desktop mode: simulated connected SSID (None = not connected) + _desktop_connected_ssid = None + @staticmethod def connect(network_module=None): """ @@ -54,15 +57,8 @@ class WifiService: Returns: bool: True if successfully connected, False otherwise """ - net = network_module if network_module else network - wlan = net.WLAN(net.STA_IF) - - # Restart WiFi hardware in case it's in a bad state - wlan.active(False) - wlan.active(True) - - # Scan for available networks - networks = wlan.scan() + # Scan for available networks using internal method + networks = WifiService._scan_networks_raw(network_module) # Sort networks by RSSI (signal strength) in descending order # RSSI is at index 3, higher values (less negative) = stronger signal @@ -104,9 +100,18 @@ class WifiService: """ print(f"WifiService: Connecting to SSID: {ssid}") - net = network_module if network_module else network time_mod = time_module if time_module else time + # Desktop mode - simulate successful connection + if not HAS_NETWORK_MODULE and network_module is None: + print("WifiService: Desktop mode, simulating connection...") + time_mod.sleep(2) + WifiService._desktop_connected_ssid = ssid + print(f"WifiService: Simulated connection to '{ssid}' successful") + return True + + net = network_module if network_module else network + try: wlan = net.WLAN(net.STA_IF) wlan.connect(ssid, password) @@ -323,20 +328,115 @@ class WifiService: return list(WifiService.access_points.keys()) @staticmethod - def save_network(ssid, password): + def _scan_networks_raw(network_module=None): + """ + Internal method to scan for available WiFi networks and return raw data. + + Args: + network_module: Network module for dependency injection (testing) + + Returns: + list: Raw network tuples from wlan.scan(), or empty list on desktop + """ + if not HAS_NETWORK_MODULE and network_module is None: + # Desktop mode - return empty (no raw data available) + return [] + + net = network_module if network_module else network + wlan = net.WLAN(net.STA_IF) + + # Restart WiFi hardware in case it is in a bad state (only if not connected) + if not wlan.isconnected(): + wlan.active(False) + wlan.active(True) + + return wlan.scan() + + @staticmethod + def scan_networks(network_module=None): + """ + Scan for available WiFi networks. + + Args: + network_module: Network module for dependency injection (testing) + + Returns: + list: List of SSIDs found, or mock data on desktop + """ + if not HAS_NETWORK_MODULE and network_module is None: + # Desktop mode - return mock SSIDs + time.sleep(1) + return ["Home WiFi", "Pretty Fly for a Wi Fi", "Winternet is coming", "The Promised LAN"] + + networks = WifiService._scan_networks_raw(network_module) + # Return unique SSIDs, filtering out empty ones and invalid lengths + ssids = list(set(n[0].decode() for n in networks if n[0])) + return [s for s in ssids if 0 < len(s) <= 32] + + @staticmethod + def get_current_ssid(network_module=None): + """ + Get the SSID of the currently connected network. + + Args: + network_module: Network module for dependency injection (testing) + + Returns: + str or None: Current SSID if connected, None otherwise + """ + if not HAS_NETWORK_MODULE and network_module is None: + # Desktop mode - return simulated connected SSID + return WifiService._desktop_connected_ssid + + net = network_module if network_module else network + try: + wlan = net.WLAN(net.STA_IF) + if wlan.isconnected(): + return wlan.config('essid') + except Exception as e: + print(f"WifiService: Error getting current SSID: {e}") + return None + + @staticmethod + def get_network_password(ssid): + """ + Get the saved password for a network. + + Args: + ssid: Network SSID + + Returns: + str or None: Password if found, None otherwise + """ + if not WifiService.access_points: + WifiService.access_points = mpos.config.SharedPreferences( + "com.micropythonos.system.wifiservice" + ).get_dict("access_points") + + ap = WifiService.access_points.get(ssid) + if ap: + return ap.get("password") + return None + + @staticmethod + def save_network(ssid, password, hidden=False): """ Save a new WiFi network credential. Args: ssid: Network SSID password: Network password + hidden: Whether this is a hidden network (always try connecting) """ # Load current saved networks prefs = mpos.config.SharedPreferences("com.micropythonos.system.wifiservice") access_points = prefs.get_dict("access_points") # Add or update the network - access_points[ssid] = {"password": password} + network_config = {"password": password} + if hidden: + network_config["hidden"] = True + access_points[ssid] = network_config # Save back to config editor = prefs.edit() @@ -346,7 +446,7 @@ class WifiService: # Update class-level cache WifiService.access_points = access_points - print(f"WifiService: Saved network '{ssid}'") + print(f"WifiService: Saved network '{ssid}' (hidden={hidden})") @staticmethod def forget_network(ssid):