webrepl: support access from LAN, without internet

Host everything on the device itself, rather than redirecting
to https://micropython.org/webrepl/ because that doesn't work when
there is not internet, including when the device is in Access Point
mode.

This was a bit slow, because of the many files and being pretty large,
but the inline_minify_webrepl.py makes this much better and brings it
down to around 1s to load the page, versus 20 seconds.

The minification also reduces the size from around 160KB to 80KB.
This commit is contained in:
Thomas Farstrike
2026-03-05 11:17:25 +01:00
parent 7d707244dd
commit 519ceaae6f
12 changed files with 6853 additions and 2 deletions
@@ -0,0 +1 @@
This folder will be filled by the inline_minify_webrepl.py script.
+2 -1
View File
@@ -223,7 +223,8 @@ TaskManager.create_task(asyncio_repl()) # only gets started after TaskManager.st
try:
import webrepl
webrepl.start(port=7890,password="MPOSweb26") # password is max 9 characters
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
except Exception as e:
print(f"Could not start webrepl - this is normal on desktop systems: {e}")
@@ -0,0 +1,5 @@
"""Web server helpers for MicroPythonOS."""
from .webrepl_http import accept_handler
__all__ = ["accept_handler"]
@@ -0,0 +1,136 @@
import os
import socket
import uio
import _webrepl
import webrepl
import websocket
WEBREPL_HTML_PATH = "builtin/html/webrepl_inlined_minified.html"
'''
# Unused as these files are minified and inlined:
#WEBREPL_HTML_PATH = "/builtin/html/webrepl.html"
WEBREPL_CONTENT_PATH = "/builtin/html/webrepl.js"
WEBREPL_TERM_PATH = "/builtin/html/term.js"
WEBREPL_CSS_PATH = "/builtin/html/webrepl.css"
WEBREPL_FILE_SAVER_PATH = "/builtin/html/FileSaver.js"
'''
WEBREPL_ASSETS = {
b"/": (WEBREPL_HTML_PATH, b"text/html"),
b"/index.html": (WEBREPL_HTML_PATH, b"text/html"),
#b"/webrepl.css": (WEBREPL_CSS_PATH, b"text/css"),
#b"/webrepl.js": (WEBREPL_CONTENT_PATH, b"application/javascript"),
#b"/term.js": (WEBREPL_TERM_PATH, b"application/javascript"),
#b"/FileSaver.js": (WEBREPL_FILE_SAVER_PATH, b"application/javascript"),
}
class _MakefileSocket:
def __init__(self, sock, raw_request):
self._sock = sock
self._raw_request = raw_request
def makefile(self, *args, **kwargs):
return uio.BytesIO(self._raw_request)
def __getattr__(self, name):
return getattr(self._sock, name)
def _read_http_request(cl):
req = cl.makefile("rwb", 0)
first_line = req.readline()
if not first_line:
return None, None, b""
raw_request = first_line
headers = {}
while True:
line = req.readline()
if not line:
break
raw_request += line
if line == b"\r\n":
break
if b":" in line:
key, value = line.split(b":", 1)
headers[key.strip().lower()] = value.strip().lower()
parts = first_line.split()
path = parts[1] if len(parts) >= 2 else b"/"
if b"?" in path:
path = path.split(b"?", 1)[0]
return path, headers, raw_request
def _is_websocket_request(headers):
connection = headers.get(b"connection", b"")
upgrade = headers.get(b"upgrade", b"")
return b"upgrade" in connection and upgrade == b"websocket"
def _send_response(cl, status, content_type, body):
cl.send(b"HTTP/1.0 " + status + b"\r\n")
cl.send(b"Server: MicroPythonOS\r\n")
cl.send(b"Content-Type: " + content_type + b"\r\n")
cl.send(b"Content-Length: %d\r\n\r\n" % len(body))
cl.send(body)
cl.close()
def _send_file_response(cl, path, content_type):
try:
with open(path, "rb") as handle:
body = handle.read()
except OSError:
_send_response(cl, b"404 Not Found", b"text/plain", b"Not Found")
return False
_send_response(cl, b"200 OK", content_type, body)
return False
def _start_webrepl_session(cl, remote_addr):
print("\nWebREPL connection from:", remote_addr)
webrepl.client_s = cl
ws = websocket.websocket(cl, True)
ws = _webrepl._webrepl(ws)
cl.setblocking(False)
if hasattr(os, "dupterm_notify"):
cl.setsockopt(socket.SOL_SOCKET, 20, os.dupterm_notify)
os.dupterm(ws)
return True
def accept_handler(listen_sock):
cl, remote_addr = listen_sock.accept()
print("\webrepl_http connection from:", remote_addr)
try:
path, headers, raw_request = _read_http_request(cl)
if not path:
cl.close()
return False
if _is_websocket_request(headers):
if not webrepl.server_handshake(_MakefileSocket(cl, raw_request)):
cl.close()
return False
return _start_webrepl_session(cl, remote_addr)
if path in WEBREPL_ASSETS:
asset_path, content_type = WEBREPL_ASSETS[path]
return _send_file_response(cl, asset_path, content_type)
_send_response(cl, b"404 Not Found", b"text/plain", b"Not Found")
return False
except Exception as exc:
print("webrepl_http: error handling connection:", exc)
try:
cl.close()
except Exception:
pass
return False