Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions lib/web_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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", "")
Expand Down Expand Up @@ -908,7 +908,7 @@ def _settings_content():
app_card = f'<div class="card"><div class="section-title">App Behavior</div>{app_html}</div>' if app_html else ""
adv_card = f'<div class="card"><details><summary><span class="caret">&#9656;</span>&#9881; Advanced</summary>{adv_html}</details></div>' if adv_html else ""
return """<div class="logo"><h1>Settings</h1><p>Configure your device</p></div>
""" + _quick_actions_card() + wifi_setup.wifi_card() + f"""
""" + _quick_actions_card() + wlan.config_card() + f"""
<form id="settingsform" onsubmit="sav(event)">
{main_card}
{app_card}
Expand Down Expand Up @@ -985,7 +985,7 @@ def _system_perf(request):

import cmd
import filemanager
import wifi_setup
import wlan


@ampule.route('/system/favicon.svg')
Expand Down
130 changes: 98 additions & 32 deletions lib/wifi_setup.py → lib/wlan.py
Original file line number Diff line number Diff line change
@@ -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():
Expand Down Expand Up @@ -84,19 +144,25 @@ def _scan_options(current_ssid=""):
wifi.radio.stop_scanning_networks()

if current_ssid and not matched_current:
networks = f"<option value='{current_ssid}' selected>{current_ssid} (not found)</option>" + networks
networks = (
f"<option value='{current_ssid}' selected>{current_ssid} (not found)</option>"
+ networks
)

networks += "<option value='__manual__'>Enter manually&hellip;</option>"
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"""<label for="ssid">Network</label>
<div class="pw-wrap">
<select id="ssid" name="ssid">{options}</select>
Expand Down Expand Up @@ -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'<p class="error-msg">{wifi_error}</p>' 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)))
Expand All @@ -176,7 +242,7 @@ def wifi_card():
channel_label = "Auto" if channel == 0 else str(channel)
error_html = status_banner()
return f"""<div class="card"><div class="section-title">WiFi</div>
{wifi_fields(settings.get("ssid", ""))}
{credentials_fields(settings.get("ssid", ""))}
<label for="wifi_power">WiFi Power</label>
<div class="range-wrap">
<input type="range" id="wifi_power" min="7" max="20" step="1" value="{power}" oninput="document.getElementById('v_wifi_power').textContent=this.value" onchange="fetch('/system/wifi/power?v='+this.value,{{method:'POST'}})">
Expand All @@ -193,28 +259,28 @@ def wifi_card():
</div>"""


def render_wifi_setup():
wifi_error = str(__main__.wifi_status)
def setup_content():
wifi_error = str(STATUS)
error_html = f'<p class="error-msg">{wifi_error}</p>' if wifi_error else ""
return f"""<div class="logo">
<h1>WiFi Setup</h1>
<p>Connect to a wireless network</p>
</div>
<div class="card">
{wifi_fields(settings.get("ssid", ""))}
{credentials_fields(settings.get("ssid", ""))}
<button class="btn btn-full" onclick="fetch('/system/wifi/connect').then(function(){{location.reload()}})">Connect</button>
{error_html}
</div>"""


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(),
)

Expand Down
44 changes: 6 additions & 38 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -184,15 +152,15 @@ 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")
else:
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]
Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion repository.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down