From 8267289dae92b9190cf59e07561173673d5a677c Mon Sep 17 00:00:00 2001 From: Simon Sawert Date: Mon, 14 Sep 2026 22:12:31 +0200 Subject: [PATCH] chore: rename and refactor wlan related code Use better naming and location for features that should be accessible across apps. When apps want to build their own reconnect loop they want to import small units from well named namespaces and well defined function names. This refactor is done now before any app wires it up, changing these once consumed by apps will be problematic. --- lib/web_interface.py | 20 ++--- lib/{wifi_setup.py => wlan.py} | 130 +++++++++++++++++++++++++-------- main.py | 44 ++--------- repository.txt | 2 +- 4 files changed, 115 insertions(+), 81 deletions(-) rename lib/{wifi_setup.py => wlan.py} (71%) diff --git a/lib/web_interface.py b/lib/web_interface.py index 9302ed1..447a3eb 100644 --- a/lib/web_interface.py +++ b/lib/web_interface.py @@ -97,9 +97,9 @@ def textbox(settings): } hidden_keys = {"ai_provider", "ai_key", "ai_model", "repository_file"} # ssid/password/wifi_power/channel live in their own WiFi card - # (wifi_setup.wifi_card()) and rotation in the Quick Actions card + # (wlan.config_card()) and rotation in the Quick Actions card # (_quick_actions_card()) -- none of them belong in this generic form. - wifi_card_keys = {"ssid", "password", "wifi_power", "channel"} + config_card_keys = {"ssid", "password", "wifi_power", "channel"} # Panel hardware -- normally auto-detected, grouped in Advanced with a warning. hardware_order = ["width", "height", "tiles", "color_correct"] other_advanced_order = ["repository_url", "enable_button"] @@ -109,7 +109,7 @@ def textbox(settings): adv_items = {} for setting in settings: if setting in hidden_keys: continue - if setting in wifi_card_keys: continue + if setting in config_card_keys: continue if setting == "rotation": continue print("Setting: ", setting) val = settings[setting] @@ -720,18 +720,18 @@ def _quick_actions_card(): def _apps_content(): # Same Quick Actions + WiFi cards as /system/settings (not the separate - # "WiFi Setup" wizard render_wifi_setup() used to be) -- one experience + # "WiFi Setup" wizard setup_content() used to be) -- one experience # for configuring the device regardless of which page you land on. if not wifi.radio.connected: - top_html = _quick_actions_card() + wifi_setup.wifi_card() + top_html = _quick_actions_card() + wlan.config_card() else: # The full card only makes sense while disconnected, but a - # lingering wifi_status (e.g. a failed post-connect settings + # lingering wlan.STATUS (e.g. a failed post-connect settings # save) still needs to surface somewhere -- this is the page the # AP setup flow lands back on after connecting, so it's the one # place that's guaranteed to be seen even if the user never # visits /system/settings afterward. - top_html = wifi_setup.status_banner() + top_html = wlan.status_banner() installed_apps = "" for app in os.listdir("/"): if app == "LICENSE": continue @@ -787,7 +787,7 @@ def webinterface_post(request): except: pass clearscreen(True) if not savesettings(settings): - __main__.wifi_status = "Couldn't save settings (read-only filesystem)" + wlan.STATUS = "Couldn't save settings (read-only filesystem)" clearscreen(False) __main__.autostart = settings.get("autostart", False) __main__.screensaver_app = settings.get("screensaver", "") @@ -908,7 +908,7 @@ def _settings_content(): app_card = f'
App Behavior
{app_html}
' if app_html else "" adv_card = f'
⚙ Advanced{adv_html}
' if adv_html else "" return """ -""" + _quick_actions_card() + wifi_setup.wifi_card() + f""" +""" + _quick_actions_card() + wlan.config_card() + f"""
{main_card} {app_card} @@ -985,7 +985,7 @@ def _system_perf(request): import cmd import filemanager -import wifi_setup +import wlan @ampule.route('/system/favicon.svg') diff --git a/lib/wifi_setup.py b/lib/wlan.py similarity index 71% rename from lib/wifi_setup.py rename to lib/wlan.py index 691981b..2a2092c 100644 --- a/lib/wifi_setup.py +++ b/lib/wlan.py @@ -1,54 +1,114 @@ +import time + import ampule -import web_interface import web_components +import web_interface -import __main__ from __main__ import ( - connect_to_network, macid, pprint, render_home_screen, + savesettings, settings, wifi, ) - -# Shared "this device isn't on WiFi yet" UI + connect flow, usable from any -# app or route via wifi_setup.needs_setup() / render_wifi_setup() / page(). -# Routes live under /system/wifi/... so they're auto-promoted (see -# ampule.py) and reachable from inside any app. +# All WiFi radio management: connecting, AP/hotspot fallback, connection +# status, and the shared "this device isn't on WiFi yet" UI + connect flow, +# usable from any app or route via wlan.is_connected() / wlan.setup_content() +# / wlan.setup_page(). Routes live under /system/wifi/... so they're +# auto-promoted (see ampule.py) and reachable from inside any app. # # Expected usage from an app: # -# import wifi_setup +# import wlan # # def main_loop(): # while True: -# if wifi_setup.needs_setup(): -# wifi_setup.show_setup_on_led() +# if not wlan.is_connected(): +# wlan.show_setup_on_led() # continue # # ... # normal app behavior # # @ampule.route('/', method='GET') # def index(request): -# if wifi_setup.needs_setup(): -# return (200, {}, header("WiFi Setup", app=True) + wifi_setup.render_wifi_setup() + footer()) +# if not wlan.is_connected(): +# return wlan.setup_page() # # return (200, {}, normal_app_page()) # -# wifi_card() is the full picker+power+channel card shared by the home -# page and /system/settings; wifi_fields() is just the network/password -# part, reused by render_wifi_setup()'s standalone page. +# wlan.config_card() is the full picker+power+channel card shared by the +# home page and /system/settings; wlan.credentials_fields() is just the +# network/password part, reused by wlan.setup_content()'s standalone page. # # apps/departures has its own separate copy, untouched for now. # # web_interface is imported eagerly since this module is only ever # imported from inside web_interface.py's own execution. +# +# macid, the boot-time socket/tx_power bring-up, and the shared HTTP +# session (pool/socket/requests) stay in main.py -- fetch_data.py reads +# macid via a `from __main__ import *` that runs before web_interface.py +# (and this module) are ever loaded, and pool/socket/requests are plain +# HTTP/TCP plumbing used directly by nearly every app, not WiFi-radio setup. -def needs_setup(): - return not wifi.radio.connected +STATUS = "" + + +def connect_to_network(timeout=False, silent=False, save=False): + # Never draws. save=True only for an explicit user-initiated connect -- + # boot/retry reuse stored settings and have nothing new to persist. + global STATUS + + if silent and wifi.radio.connected: + return time.monotonic() + + STATUS = "" + print("Connecting...") + + try: + channel = settings.get("channel", 0) + if channel: + wifi.radio.connect( + str(settings["ssid"]), + str(settings["password"]), + channel=int(channel), + timeout=timeout, + ) + else: + wifi.radio.connect( + str(settings["ssid"]), str(settings["password"]), timeout=timeout + ) + if save and wifi.radio.connected: + if not savesettings(settings): + STATUS = "Connected, but couldn't save settings (read-only filesystem)" + + except Exception as e: + if "unknown failure" in str(e).lower(): + e = "Router distance!" + if "no network with" in str(e).lower(): + e = "Wrong WiFi name" + if "authentication failure" in str(e).lower(): + e = "Wrong password" + + print(e) + STATUS = str(e) + + return time.monotonic() + + +def start_hotspot(): + try: + wifi.radio.start_ap(ssid=macid) + render_home_screen() + except Exception as e: + pprint(str(e)) + + +def is_connected(): + return wifi.radio.connected def show_setup_on_led(): @@ -84,19 +144,25 @@ def _scan_options(current_ssid=""): wifi.radio.stop_scanning_networks() if current_ssid and not matched_current: - networks = f"" + networks + networks = ( + f"" + + networks + ) networks += "" return networks -def wifi_fields(current_ssid=""): +def credentials_fields(current_ssid=""): """Network picker + password field, shared by the AP-setup page and /system/settings. Only auto-scans while disconnected -- scanning while connected can drop the radio off its own AP, so once connected only the rescan button (⟳) scans.""" - options = _scan_options(current_ssid) if not wifi.radio.connected else _current_options(current_ssid) - + options = ( + _scan_options(current_ssid) + if not wifi.radio.connected + else _current_options(current_ssid) + ) return f"""
@@ -158,12 +224,12 @@ def wifi_fields(current_ssid=""): def status_banner(): - """Last wifi_status error on its own, for pages that don't show the full wifi_card().""" - wifi_error = str(__main__.wifi_status) + """Last STATUS error on its own, for pages that don't show the full config_card().""" + wifi_error = str(STATUS) return f'

{wifi_error}

' if wifi_error else "" -def wifi_card(): +def config_card(): """Full WiFi card (picker, power, channel, connect, error) shared by home and /system/settings.""" try: power = int(float(settings.get("wifi_power", 9))) @@ -176,7 +242,7 @@ def wifi_card(): channel_label = "Auto" if channel == 0 else str(channel) error_html = status_banner() return f"""
WiFi
-{wifi_fields(settings.get("ssid", ""))} +{credentials_fields(settings.get("ssid", ""))}
@@ -193,28 +259,28 @@ def wifi_card():
""" -def render_wifi_setup(): - wifi_error = str(__main__.wifi_status) +def setup_content(): + wifi_error = str(STATUS) error_html = f'

{wifi_error}

' if wifi_error else "" return f"""
- {wifi_fields(settings.get("ssid", ""))} + {credentials_fields(settings.get("ssid", ""))} {error_html}
""" -def page(title="WiFi Setup"): +def setup_page(title="WiFi Setup"): # Convenience for the common case: an app that already uses - # web_interface's header()/footer() can just return wifi_setup.page(). + # web_interface's header()/footer() can just return wlan.setup_page(). return ( 200, {}, web_interface.header(title, app=True) - + render_wifi_setup() + + setup_content() + web_interface.footer(), ) diff --git a/main.py b/main.py index 928fc7b..81b454a 100644 --- a/main.py +++ b/main.py @@ -58,42 +58,10 @@ def logo_anim_step(): socket.listen(5) socket_timeout = 5 macid = "matrixbox-" + "".join([hex(i) for i in wifi.radio.mac_address]).replace("0x","")[:3] # mac-id för hotspot -wifi_status = "" ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) requests = adafruit_requests.Session(pool, ssl_context) screensaver = time.monotonic() -def start_hotspot(): - try: - wifi.radio.start_ap(ssid=macid) - render_home_screen() - except Exception as e: pprint(str(e)) - -def connect_to_network(timeout=False, silent=False, save=False): - # Never draws. save=True only for an explicit user-initiated connect -- - # boot/retry reuse stored settings and have nothing new to persist. - global wifi_status - if silent and wifi.radio.connected: - return time.monotonic() - wifi_status = "" - print("Connecting...") - try: - channel = settings.get("channel", 0) - if channel: - wifi.radio.connect(str(settings["ssid"]), str(settings["password"]), channel=int(channel), timeout=timeout) - else: - wifi.radio.connect(str(settings["ssid"]), str(settings["password"]), timeout=timeout) - if save and wifi.radio.connected: - if not savesettings(settings): - wifi_status = "Connected, but couldn't save settings (read-only filesystem)" - except Exception as e: - if "unknown failure" in str(e).lower(): e = "Router distance!" - if "no network with" in str(e).lower(): e = "Wrong WIFI name" - if "authentication failure" in str(e).lower(): e = "Wrong password" - print(e) - wifi_status = str(e) - return time.monotonic() - @ampule.route("/exit", method="GET") def webinterface(request): load_settings.app_running = False @@ -184,7 +152,7 @@ def render_home_screen(): if wifi.radio.connected: _wifi_address = f"IP: {wifi.radio.ipv4_address}" if wifi.radio.ipv4_address else "OFFLINE" pprint(_wifi_address, line=1) - if wifi_status: + if wlan.STATUS: # Full message is in the Settings error box; screen is too narrow for it. pprint("Read-only filesystem", line=2, color="red") pprint("Restart to fix", line=3, color="red") @@ -192,7 +160,7 @@ def render_home_screen(): pprint("Select app:", line=2) show_first_app() elif wifi.radio.ap_active: - wifi_setup.show_setup_on_led() + wlan.show_setup_on_led() def next_program_in_list(run=False): try: load_settings.installed_apps_list[1] @@ -220,14 +188,14 @@ def check_for_button_next_program(): wifi.radio.tx_power = float(settings["wifi_power"]) from web_interface import * -import wifi_setup -connect_to_network() +import wlan +wlan.connect_to_network() while 1: print("Entered main loop") while not wifi.radio.connected and not wifi.radio.ap_active: check_network_again_timer = time.monotonic() - start_hotspot() + wlan.start_hotspot() while wifi.radio.ap_active and not wifi.radio.connected: ampule.listen(socket) @@ -239,7 +207,7 @@ def check_for_button_next_program(): print("Attempting... " + str(wifi.radio.tx_power)) wifi.radio.tx_power += 1 if wifi.radio.tx_power == 21: wifi.radio.tx_power = 18 - check_network_again_timer = connect_to_network(timeout=3, silent=True) + check_network_again_timer = wlan.connect_to_network(timeout=3, silent=True) if wifi.radio.connected: wifi.radio.stop_ap() while wifi.radio.connected or wifi.radio.ap_active: diff --git a/repository.txt b/repository.txt index c991d9e..d32d692 100644 --- a/repository.txt +++ b/repository.txt @@ -5,7 +5,7 @@ "main.py", "lib/web_interface.py", "lib/web_components.py", - "lib/wifi_setup.py", + "lib/wlan.py", "lib/ampule.py", "lib/load_screen.py", "lib/downloader.py",