Add WebServer settings app

This commit is contained in:
Thomas Farstrike
2026-03-17 19:30:34 +01:00
parent 5b50ce8528
commit 8ac4016e33
11 changed files with 458 additions and 7 deletions
+2 -1
View File
@@ -19,6 +19,7 @@ from .build_info import BuildInfo
# Battery manager (imported early for UI dependencies)
from .battery_manager import BatteryManager
from .webserver.webserver import WebServer
# Common activities
from .app.activities.chooser import ChooserActivity
@@ -67,7 +68,7 @@ __all__ = [
"Activity",
"SharedPreferences",
"ConnectivityManager", "DownloadManager", "WifiService", "AudioManager", "Intent",
"ActivityNavigator", "AppManager", "TaskManager", "CameraManager", "BatteryManager",
"ActivityNavigator", "AppManager", "TaskManager", "CameraManager", "BatteryManager", "WebServer",
# Device and build info
"DeviceInfo", "BuildInfo",
# Common activities
+3 -4
View File
@@ -229,11 +229,10 @@ async def asyncio_repl():
TaskManager.create_task(asyncio_repl()) # only gets started after TaskManager.start()
try:
import webrepl
from mpos.webserver import accept_handler as webrepl_accept_handler
webrepl.start(port=7890, password="MPOSweb26", accept_handler=webrepl_accept_handler) # password is max 9 characters
from mpos import WebServer
WebServer.auto_start()
except Exception as e:
print(f"Could not start webrepl - this is normal on desktop systems: {e}")
print(f"Could not start webserver - this is normal on desktop systems: {e}")
async def ota_rollback_cancel():
try:
@@ -1,5 +1,6 @@
"""Web server helpers for MicroPythonOS."""
from .webrepl_http import accept_handler
from .webserver import WebServer
__all__ = ["accept_handler"]
__all__ = ["accept_handler", "WebServer"]
@@ -0,0 +1,182 @@
# This module should be imported from REPL, not run from command line.
import binascii
import hashlib
from micropython import const
try:
import network
except ImportError:
network = None
import os
import socket
import sys
import websocket
import _webrepl
listen_s = None
client_s = None
DEBUG = 0
_DEFAULT_STATIC_HOST = const("https://micropython.org/webrepl/")
static_host = _DEFAULT_STATIC_HOST
def server_handshake(cl):
req = cl.makefile("rwb", 0)
# Skip HTTP GET line.
l = req.readline()
if DEBUG:
sys.stdout.write(repr(l))
webkey = None
upgrade = False
websocket = False
while True:
l = req.readline()
if not l:
# EOF in headers.
return False
if l == b"\r\n":
break
if DEBUG:
sys.stdout.write(l)
h, v = [x.strip() for x in l.split(b":", 1)]
if DEBUG:
print((h, v))
if h == b"Sec-WebSocket-Key":
webkey = v
elif h == b"Connection" and b"Upgrade" in v:
upgrade = True
elif h == b"Upgrade" and v == b"websocket":
websocket = True
if not (upgrade and websocket and webkey):
return False
if DEBUG:
print("Sec-WebSocket-Key:", webkey, len(webkey))
d = hashlib.sha1(webkey)
d.update(b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
respkey = d.digest()
respkey = binascii.b2a_base64(respkey)[:-1]
if DEBUG:
print("respkey:", respkey)
cl.send(
b"""\
HTTP/1.1 101 Switching Protocols\r
Upgrade: websocket\r
Connection: Upgrade\r
Sec-WebSocket-Accept: """
)
cl.send(respkey)
cl.send("\r\n\r\n")
return True
def send_html(cl):
cl.send(
b"""\
HTTP/1.0 200 OK\r
\r
<base href=\""""
)
cl.send(static_host)
cl.send(
b"""\"></base>\r
<script src="webrepl_content.js"></script>\r
"""
)
cl.close()
def setup_conn(port, accept_handler):
global listen_s
listen_s = socket.socket()
listen_s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
ai = socket.getaddrinfo("0.0.0.0", port)
addr = ai[0][4]
listen_s.bind(addr)
listen_s.listen(1)
if accept_handler:
listen_s.setsockopt(socket.SOL_SOCKET, 20, accept_handler)
if network:
for i in (network.WLAN.IF_AP, network.WLAN.IF_STA):
iface = network.WLAN(i)
if iface.active():
print("WebREPL server started on http://%s:%d/" % (iface.ifconfig()[0], port))
return listen_s
def accept_conn(listen_sock):
global client_s
cl, remote_addr = listen_sock.accept()
if not server_handshake(cl):
send_html(cl)
return False
prev = os.dupterm(None)
os.dupterm(prev)
if prev:
print("\nConcurrent WebREPL connection from", remote_addr, "rejected")
cl.close()
return False
print("\nWebREPL connection from:", remote_addr)
client_s = cl
ws = websocket.websocket(cl, True)
ws = _webrepl._webrepl(ws)
cl.setblocking(False)
# notify REPL on socket incoming data (ESP32/ESP8266-only)
if hasattr(os, "dupterm_notify"):
cl.setsockopt(socket.SOL_SOCKET, 20, os.dupterm_notify)
os.dupterm(ws)
return True
def stop():
global listen_s, client_s
os.dupterm(None)
if client_s:
client_s.close()
if listen_s:
listen_s.close()
def start(port=8266, password=None, accept_handler=accept_conn):
global static_host
stop()
webrepl_pass = password
if webrepl_pass is None:
try:
import webrepl_cfg
webrepl_pass = webrepl_cfg.PASS
if hasattr(webrepl_cfg, "BASE"):
static_host = webrepl_cfg.BASE
except:
print("WebREPL is not configured, run 'import webrepl_setup'")
_webrepl.password(webrepl_pass)
s = setup_conn(port, accept_handler)
if accept_handler is None:
print("Starting webrepl in foreground mode")
# Run accept_conn to serve HTML until we get a websocket connection.
while not accept_conn(s):
pass
elif password is None:
print("Started webrepl in normal mode")
else:
print("Started webrepl in manual override mode")
def start_foreground(port=8266, password=None):
start(port, password, None)
@@ -3,7 +3,7 @@ import socket
import uio
import _webrepl
import webrepl
from . import webrepl
import websocket
WEBREPL_HTML_PATH = "builtin/html/webrepl_inlined_minified.html"
@@ -0,0 +1,115 @@
"""WebServer control for MicroPythonOS."""
from ..config import SharedPreferences
from .webrepl_http import accept_handler
class WebServer:
PREFS_NAMESPACE = "com.micropythonos.webserver"
DEFAULTS = {
"autostart": "False",
"port": "7890",
"password": "MPOSweb26",
}
_started = False
_port = None
_password = None
_autostart = None
_last_error = None
@classmethod
def _prefs(cls):
return SharedPreferences(cls.PREFS_NAMESPACE, defaults=cls.DEFAULTS)
@classmethod
def _parse_bool(cls, value):
return str(value).lower() in ("true", "1", "yes", "on")
@classmethod
def _parse_port(cls, value):
try:
return int(value)
except Exception:
return int(cls.DEFAULTS["port"])
@classmethod
def _sanitize_password(cls, value):
if not value:
value = cls.DEFAULTS["password"]
if len(value) > 9:
value = value[:9]
return value
@classmethod
def load_settings(cls):
prefs = cls._prefs()
cls._autostart = cls._parse_bool(prefs.get_string("autostart", cls.DEFAULTS["autostart"]))
cls._port = cls._parse_port(prefs.get_string("port", cls.DEFAULTS["port"]))
cls._password = cls._sanitize_password(prefs.get_string("password", cls.DEFAULTS["password"]))
@classmethod
def status(cls):
cls.load_settings()
return {
"state": "started" if cls._started else "stopped",
"started": cls._started,
"port": cls._port,
"password": cls._password,
"autostart": cls._autostart,
"last_error": cls._last_error,
}
@classmethod
def is_started(cls):
return cls._started
@classmethod
def start(cls):
cls.load_settings()
try:
from . import webrepl
webrepl.start(port=cls._port, password=cls._password, accept_handler=accept_handler)
cls._started = True
cls._last_error = None
print(f"WebServer started on port {cls._port}")
return True
except Exception as exc:
cls._last_error = exc
cls._started = False
print(f"WebServer start failed: {exc}")
return False
@classmethod
def stop(cls):
try:
from . import webrepl
if hasattr(webrepl, "stop"):
webrepl.stop()
cls._started = False
cls._last_error = None
print("WebServer stopped")
return True
except Exception as exc:
cls._last_error = exc
print(f"WebServer stop failed: {exc}")
return False
@classmethod
def apply_settings(cls, restart_if_running=True):
was_running = cls._started
cls.load_settings()
if was_running and restart_if_running:
cls.stop()
cls.start()
return cls.status()
@classmethod
def auto_start(cls):
cls.load_settings()
if cls._autostart:
return cls.start()
print("WebServer autostart disabled")
return False