Skip to content

Commit 8ac4016

Browse files
Add WebServer settings app
1 parent 5b50ce8 commit 8ac4016

11 files changed

Lines changed: 458 additions & 7 deletions

File tree

c_mpos/micropython.cmake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ set(MPOS_C_SOURCES
1818
${CMAKE_CURRENT_LIST_DIR}/quirc/lib/version_db.c
1919
${CMAKE_CURRENT_LIST_DIR}/quirc/lib/decode.c
2020
${CMAKE_CURRENT_LIST_DIR}/quirc/lib/quirc.c
21+
# ${CMAKE_CURRENT_LIST_DIR}/../lvgl_micropython/lib/micropython/extmod/modwebrepl.c
2122
# ${CMAKE_CURRENT_LIST_DIR}/src/font_Noto_Sans_sat_emojis_compressed.c
2223
)
2324

internal_filesystem/builtin/apps/com.micropythonos.settings/assets/settings.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ def onCreate(self):
1818
AppManager.start_app("com.micropythonos.hotspot")
1919

2020

21+
class LaunchWebServer(Activity):
22+
23+
def onCreate(self):
24+
AppManager.start_app("com.micropythonos.webserver")
25+
26+
2127
class Settings(SettingsActivity):
2228

2329
"""Override getIntent to provide prefs and settings via Intent extras"""
@@ -53,6 +59,7 @@ def getIntent(self):
5359
intent.putExtra("settings", [
5460
{"title": "Wi-Fi", "key": "wifi_settings", "ui": "activity", "activity_class": LaunchWiFi},
5561
{"title": "Hotspot", "key": "hotspot_settings", "ui": "activity", "activity_class": LaunchHotspot},
62+
{"title": "WebServer", "key": "webserver_settings", "ui": "activity", "activity_class": LaunchWebServer},
5663
# Basic settings, alphabetically:
5764
{"title": "Light/Dark Theme", "key": "theme_light_dark", "ui": "radiobuttons", "ui_options": [("Light", "light"), ("Dark", "dark")], "changed_callback": self.theme_changed},
5865
{"title": "Theme Color", "key": "theme_primary_color", "placeholder": "HTML hex color, like: EC048C", "ui": "dropdown", "ui_options": theme_colors, "changed_callback": self.theme_changed, "default_value": AppearanceManager.DEFAULT_PRIMARY_COLOR},
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "WebServer",
3+
"publisher": "MicroPythonOS",
4+
"short_description": "Configure and control the WebServer.",
5+
"long_description": "Configure WebServer settings, start or stop the WebREPL web server.",
6+
"icon_url": "https://apps.micropythonos.com/apps/com.micropythonos.webserver/icons/com.micropythonos.webserver_0.1.0_64x64.png",
7+
"download_url": "https://apps.micropythonos.com/apps/com.micropythonos.webserver/mpks/com.micropythonos.webserver_0.1.0.mpk",
8+
"fullname": "com.micropythonos.webserver",
9+
"version": "0.1.0",
10+
"category": "networking",
11+
"activities": [
12+
{
13+
"entrypoint": "assets/webserver.py",
14+
"classname": "WebServerApp",
15+
"intent_filters": [
16+
{
17+
"action": "main",
18+
"category": "launcher"
19+
}
20+
]
21+
}
22+
]
23+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import lvgl as lv
2+
3+
from mpos import Activity, DisplayMetrics, Intent, SettingsActivity, SharedPreferences, WebServer
4+
5+
6+
class WebServerApp(Activity):
7+
status_label = None
8+
detail_label = None
9+
action_button = None
10+
action_label = None
11+
settings_button = None
12+
prefs = None
13+
14+
def onCreate(self):
15+
self.prefs = SharedPreferences(WebServer.PREFS_NAMESPACE, defaults=WebServer.DEFAULTS)
16+
screen = lv.obj()
17+
screen.set_style_border_width(0, lv.PART.MAIN)
18+
screen.set_style_pad_all(DisplayMetrics.pct_of_width(3), lv.PART.MAIN)
19+
screen.set_flex_flow(lv.FLEX_FLOW.COLUMN)
20+
21+
header = lv.label(screen)
22+
header.set_text("WebServer")
23+
header.set_style_text_font(lv.font_montserrat_20, lv.PART.MAIN)
24+
25+
self.status_label = lv.label(screen)
26+
self.status_label.set_style_text_font(lv.font_montserrat_14, lv.PART.MAIN)
27+
self.status_label.set_long_mode(lv.label.LONG_MODE.WRAP)
28+
self.status_label.set_width(lv.pct(100))
29+
30+
self.detail_label = lv.label(screen)
31+
self.detail_label.set_style_text_font(lv.font_montserrat_12, lv.PART.MAIN)
32+
self.detail_label.set_long_mode(lv.label.LONG_MODE.WRAP)
33+
self.detail_label.set_width(lv.pct(100))
34+
35+
button_row = lv.obj(screen)
36+
button_row.set_width(lv.pct(100))
37+
button_row.set_height(lv.SIZE_CONTENT)
38+
button_row.set_style_border_width(0, lv.PART.MAIN)
39+
button_row.set_style_pad_all(0, lv.PART.MAIN)
40+
button_row.set_flex_flow(lv.FLEX_FLOW.ROW)
41+
button_row.set_style_flex_main_place(lv.FLEX_ALIGN.SPACE_BETWEEN, lv.PART.MAIN)
42+
43+
self.action_button = lv.button(button_row)
44+
self.action_button.set_size(lv.pct(45), lv.SIZE_CONTENT)
45+
self.action_button.add_event_cb(self.toggle_webserver, lv.EVENT.CLICKED, None)
46+
self.action_label = lv.label(self.action_button)
47+
self.action_label.center()
48+
49+
self.settings_button = lv.button(button_row)
50+
self.settings_button.set_size(lv.pct(45), lv.SIZE_CONTENT)
51+
self.settings_button.add_event_cb(self.open_settings, lv.EVENT.CLICKED, None)
52+
settings_label = lv.label(self.settings_button)
53+
settings_label.set_text("Settings")
54+
settings_label.center()
55+
56+
self.setContentView(screen)
57+
58+
def onResume(self, screen):
59+
super().onResume(screen)
60+
self.refresh_status()
61+
62+
def refresh_status(self):
63+
status = WebServer.status()
64+
state_text = "Running" if status.get("started") else "Stopped"
65+
self.status_label.set_text(f"Status: {state_text}")
66+
autostart_text = "On" if status.get("autostart") else "Off"
67+
port = status.get("port")
68+
self.detail_label.set_text(f"Port: {port}\nAutostart: {autostart_text}")
69+
70+
button_text = "Stop" if status.get("started") else "Start"
71+
self.action_label.set_text(button_text)
72+
self.action_label.center()
73+
74+
def toggle_webserver(self, event):
75+
if WebServer.is_started():
76+
WebServer.stop()
77+
else:
78+
WebServer.start()
79+
self.refresh_status()
80+
81+
def open_settings(self, event):
82+
intent = Intent(activity_class=SettingsActivity)
83+
intent.putExtra("prefs", self.prefs)
84+
intent.putExtra(
85+
"settings",
86+
[
87+
{
88+
"title": "Autostart",
89+
"key": "autostart",
90+
"ui": "radiobuttons",
91+
"ui_options": [("On", "True"), ("Off", "False")],
92+
"changed_callback": self.settings_changed,
93+
},
94+
{
95+
"title": "Port",
96+
"key": "port",
97+
"placeholder": "WebServer port, e.g. 7890",
98+
"changed_callback": self.settings_changed,
99+
},
100+
{
101+
"title": "Password",
102+
"key": "password",
103+
"placeholder": "Max 9 characters",
104+
"changed_callback": self.settings_changed,
105+
},
106+
],
107+
)
108+
self.startActivity(intent)
109+
110+
def settings_changed(self, new_value):
111+
WebServer.apply_settings(restart_if_running=True)
112+
self.refresh_status()

internal_filesystem/lib/mpos/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
# Battery manager (imported early for UI dependencies)
2121
from .battery_manager import BatteryManager
22+
from .webserver.webserver import WebServer
2223

2324
# Common activities
2425
from .app.activities.chooser import ChooserActivity
@@ -67,7 +68,7 @@
6768
"Activity",
6869
"SharedPreferences",
6970
"ConnectivityManager", "DownloadManager", "WifiService", "AudioManager", "Intent",
70-
"ActivityNavigator", "AppManager", "TaskManager", "CameraManager", "BatteryManager",
71+
"ActivityNavigator", "AppManager", "TaskManager", "CameraManager", "BatteryManager", "WebServer",
7172
# Device and build info
7273
"DeviceInfo", "BuildInfo",
7374
# Common activities

internal_filesystem/lib/mpos/main.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -229,11 +229,10 @@ async def asyncio_repl():
229229
TaskManager.create_task(asyncio_repl()) # only gets started after TaskManager.start()
230230

231231
try:
232-
import webrepl
233-
from mpos.webserver import accept_handler as webrepl_accept_handler
234-
webrepl.start(port=7890, password="MPOSweb26", accept_handler=webrepl_accept_handler) # password is max 9 characters
232+
from mpos import WebServer
233+
WebServer.auto_start()
235234
except Exception as e:
236-
print(f"Could not start webrepl - this is normal on desktop systems: {e}")
235+
print(f"Could not start webserver - this is normal on desktop systems: {e}")
237236

238237
async def ota_rollback_cancel():
239238
try:
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Web server helpers for MicroPythonOS."""
22

33
from .webrepl_http import accept_handler
4+
from .webserver import WebServer
45

5-
__all__ = ["accept_handler"]
6+
__all__ = ["accept_handler", "WebServer"]
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# This module should be imported from REPL, not run from command line.
2+
import binascii
3+
import hashlib
4+
from micropython import const
5+
try:
6+
import network
7+
except ImportError:
8+
network = None
9+
import os
10+
import socket
11+
import sys
12+
import websocket
13+
import _webrepl
14+
15+
listen_s = None
16+
client_s = None
17+
18+
DEBUG = 0
19+
20+
_DEFAULT_STATIC_HOST = const("https://micropython.org/webrepl/")
21+
static_host = _DEFAULT_STATIC_HOST
22+
23+
24+
def server_handshake(cl):
25+
req = cl.makefile("rwb", 0)
26+
# Skip HTTP GET line.
27+
l = req.readline()
28+
if DEBUG:
29+
sys.stdout.write(repr(l))
30+
31+
webkey = None
32+
upgrade = False
33+
websocket = False
34+
35+
while True:
36+
l = req.readline()
37+
if not l:
38+
# EOF in headers.
39+
return False
40+
if l == b"\r\n":
41+
break
42+
if DEBUG:
43+
sys.stdout.write(l)
44+
h, v = [x.strip() for x in l.split(b":", 1)]
45+
if DEBUG:
46+
print((h, v))
47+
if h == b"Sec-WebSocket-Key":
48+
webkey = v
49+
elif h == b"Connection" and b"Upgrade" in v:
50+
upgrade = True
51+
elif h == b"Upgrade" and v == b"websocket":
52+
websocket = True
53+
54+
if not (upgrade and websocket and webkey):
55+
return False
56+
57+
if DEBUG:
58+
print("Sec-WebSocket-Key:", webkey, len(webkey))
59+
60+
d = hashlib.sha1(webkey)
61+
d.update(b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
62+
respkey = d.digest()
63+
respkey = binascii.b2a_base64(respkey)[:-1]
64+
if DEBUG:
65+
print("respkey:", respkey)
66+
67+
cl.send(
68+
b"""\
69+
HTTP/1.1 101 Switching Protocols\r
70+
Upgrade: websocket\r
71+
Connection: Upgrade\r
72+
Sec-WebSocket-Accept: """
73+
)
74+
cl.send(respkey)
75+
cl.send("\r\n\r\n")
76+
77+
return True
78+
79+
80+
def send_html(cl):
81+
cl.send(
82+
b"""\
83+
HTTP/1.0 200 OK\r
84+
\r
85+
<base href=\""""
86+
)
87+
cl.send(static_host)
88+
cl.send(
89+
b"""\"></base>\r
90+
<script src="webrepl_content.js"></script>\r
91+
"""
92+
)
93+
cl.close()
94+
95+
96+
def setup_conn(port, accept_handler):
97+
global listen_s
98+
listen_s = socket.socket()
99+
listen_s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
100+
101+
ai = socket.getaddrinfo("0.0.0.0", port)
102+
addr = ai[0][4]
103+
104+
listen_s.bind(addr)
105+
listen_s.listen(1)
106+
if accept_handler:
107+
listen_s.setsockopt(socket.SOL_SOCKET, 20, accept_handler)
108+
if network:
109+
for i in (network.WLAN.IF_AP, network.WLAN.IF_STA):
110+
iface = network.WLAN(i)
111+
if iface.active():
112+
print("WebREPL server started on http://%s:%d/" % (iface.ifconfig()[0], port))
113+
return listen_s
114+
115+
116+
def accept_conn(listen_sock):
117+
global client_s
118+
cl, remote_addr = listen_sock.accept()
119+
120+
if not server_handshake(cl):
121+
send_html(cl)
122+
return False
123+
124+
prev = os.dupterm(None)
125+
os.dupterm(prev)
126+
if prev:
127+
print("\nConcurrent WebREPL connection from", remote_addr, "rejected")
128+
cl.close()
129+
return False
130+
print("\nWebREPL connection from:", remote_addr)
131+
client_s = cl
132+
133+
ws = websocket.websocket(cl, True)
134+
ws = _webrepl._webrepl(ws)
135+
cl.setblocking(False)
136+
# notify REPL on socket incoming data (ESP32/ESP8266-only)
137+
if hasattr(os, "dupterm_notify"):
138+
cl.setsockopt(socket.SOL_SOCKET, 20, os.dupterm_notify)
139+
os.dupterm(ws)
140+
141+
return True
142+
143+
144+
def stop():
145+
global listen_s, client_s
146+
os.dupterm(None)
147+
if client_s:
148+
client_s.close()
149+
if listen_s:
150+
listen_s.close()
151+
152+
153+
def start(port=8266, password=None, accept_handler=accept_conn):
154+
global static_host
155+
stop()
156+
webrepl_pass = password
157+
if webrepl_pass is None:
158+
try:
159+
import webrepl_cfg
160+
161+
webrepl_pass = webrepl_cfg.PASS
162+
if hasattr(webrepl_cfg, "BASE"):
163+
static_host = webrepl_cfg.BASE
164+
except:
165+
print("WebREPL is not configured, run 'import webrepl_setup'")
166+
167+
_webrepl.password(webrepl_pass)
168+
s = setup_conn(port, accept_handler)
169+
170+
if accept_handler is None:
171+
print("Starting webrepl in foreground mode")
172+
# Run accept_conn to serve HTML until we get a websocket connection.
173+
while not accept_conn(s):
174+
pass
175+
elif password is None:
176+
print("Started webrepl in normal mode")
177+
else:
178+
print("Started webrepl in manual override mode")
179+
180+
181+
def start_foreground(port=8266, password=None):
182+
start(port, password, None)

internal_filesystem/lib/mpos/webserver/webrepl_http.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import uio
44

55
import _webrepl
6-
import webrepl
6+
from . import webrepl
77
import websocket
88

99
WEBREPL_HTML_PATH = "builtin/html/webrepl_inlined_minified.html"

0 commit comments

Comments
 (0)