WiFi app: delegate to WiFiService where possible

This commit is contained in:
Thomas Farstrike
2025-12-19 14:39:02 +01:00
parent 59bbcfb46e
commit 73cba70d55
2 changed files with 204 additions and 145 deletions
@@ -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()
+113 -13
View File
@@ -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):