feat(led-tool): browser settings editor with Web Serial

Add static editor, host_ports filtering, Flask /editor and REST APIs
for ports and led-cli read/write; document standalone and embedded use.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-18 14:54:15 +12:00
parent 1edcb8b1f7
commit f74e21f206
6 changed files with 1141 additions and 2 deletions

114
web.py
View File

@@ -19,6 +19,7 @@ from flask import (
redirect,
url_for,
flash,
send_from_directory,
)
@@ -696,10 +697,119 @@ def handle_action():
)
def _static_dir() -> str:
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
@app.route("/ports")
def api_list_ports():
from host_ports import filter_port_dicts
from serial.tools import list_ports
ports = filter_port_dicts(
[
{"device": i.device, "description": i.description, "hwid": i.hwid}
for i in list_ports.comports()
]
)
cli = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cli.py")
return json.dumps({"ports": ports, "led_cli_exists": os.path.exists(cli)})
@app.route("/settings", methods=["GET"])
def api_read_settings():
import subprocess
import sys
port = (request.args.get("port") or "").strip()
if not port:
return json.dumps({"error": "port is required"}), 400
cli = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cli.py")
result = subprocess.run(
[sys.executable, cli, "--port", port, "--show"],
capture_output=True,
text=True,
timeout=180,
cwd=os.path.dirname(cli),
)
settings = None
try:
settings = json.loads((result.stdout or "").strip())
except json.JSONDecodeError:
pass
return json.dumps(
{
"ok": result.returncode == 0,
"returncode": result.returncode,
"stdout": result.stdout,
"stderr": result.stderr,
"settings": settings,
}
)
@app.route("/settings", methods=["POST"])
def api_write_settings():
import subprocess
import sys
data = request.get_json(silent=True) or {}
port = str(data.get("port") or "").strip()
if not port:
return json.dumps({"error": "port is required"}), 400
cli = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cli.py")
cmd = [sys.executable, cli, "--port", port]
flag_map = (
("name", "--name"),
("led_pin", "--pin"),
("num_leds", "--leds"),
("brightness", "--brightness"),
("transport", "--transport"),
("ssid", "--ssid"),
("password", "--wifi-password"),
("wifi_channel", "--wifi-channel"),
("default", "--default"),
)
for key, flag in flag_map:
val = data.get(key)
if val is None:
continue
s = str(val).strip()
if s:
cmd.extend([flag, s])
cmd.append("--follow")
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=180,
cwd=os.path.dirname(cli),
)
return json.dumps(
{
"ok": result.returncode == 0,
"returncode": result.returncode,
"stdout": result.stdout,
"stderr": result.stderr,
}
)
@app.route("/editor")
def settings_editor():
"""Static settings editor (Web Serial + host serial). Prefer this over the legacy form."""
return send_from_directory(_static_dir(), "settings_editor.html")
@app.route("/static/<path:filename>")
def settings_static(filename):
return send_from_directory(_static_dir(), filename)
def main() -> None:
# Bind to all interfaces so you can reach it from your LAN:
# python web_app.py
# Then open: http://<pi-ip>:5000/
# python web.py
# Then open: http://<pi-ip>:5000/editor
app.run(host="0.0.0.0", port=5000, debug=False)