#!/usr/bin/env python3
"""
LED Bar Configuration Web App
Flask-based web UI for downloading, editing, and uploading settings.json
to/from MicroPython devices via mpremote.
"""
import json
import tempfile
import os
from pathlib import Path
from device import copy_file, DeviceConnection
from flask import (
Flask,
render_template_string,
request,
redirect,
url_for,
flash,
)
app = Flask(__name__)
app.secret_key = "change-me-in-production"
SETTINGS_CONFIG = [
("led_pin", "LED Pin", "number"),
("num_leds", "Number of LEDs", "number"),
("color_order", "Color Order", "choice", ["rgb", "rbg", "grb", "gbr", "brg", "bgr"]),
("name", "Device Name", "text"),
("pattern", "Pattern", "text"),
("delay", "Delay (ms)", "number"),
("brightness", "Brightness", "number"),
("n1", "N1", "number"),
("n2", "N2", "number"),
("n3", "N3", "number"),
("n4", "N4", "number"),
("n5", "N5", "number"),
("n6", "N6", "number"),
("ap_password", "AP Password", "text"),
("id", "ID", "number"),
("debug", "Debug Mode", "choice", ["True", "False"]),
]
def download_settings(device: str) -> dict:
"""Download settings.json from the device."""
temp_file = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False)
temp_path = temp_file.name
temp_file.close()
try:
copy_file(from_device=True, device=device, remote_path="settings.json", local_path=temp_path)
with open(temp_path, "r", encoding="utf-8") as f:
return json.load(f)
finally:
if os.path.exists(temp_path):
try:
os.unlink(temp_path)
except OSError:
pass
def upload_settings(device: str, settings: dict) -> None:
"""Upload settings.json to the device and reset device."""
temp_file = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False)
temp_path = temp_file.name
try:
json.dump(settings, temp_file, indent=2)
temp_file.close()
copy_file(from_device=False, device=device, remote_path="settings.json", local_path=temp_path)
# Reset device (best effort)
try:
conn = DeviceConnection(device)
conn.connect()
conn.reset()
conn.disconnect()
except Exception:
pass
finally:
if os.path.exists(temp_path):
try:
os.unlink(temp_path)
except OSError:
pass
def parse_settings_from_form(form) -> dict:
settings = {}
for cfg in SETTINGS_CONFIG:
key = cfg[0]
raw = (form.get(key) or "").strip()
if raw == "":
continue
if key in ["led_pin", "num_leds", "delay", "brightness", "id", "n1", "n2", "n3", "n4", "n5", "n6"]:
try:
settings[key] = int(raw)
except ValueError:
settings[key] = raw
elif key == "debug":
settings[key] = raw == "True"
else:
settings[key] = raw
return settings
TEMPLATE = """
LED Bar Configuration
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
{{ message }}
{% endfor %}
{% endif %}
{% endwith %}
"""
@app.route("/", methods=["GET"])
def index():
return render_template_string(
TEMPLATE,
device="/dev/ttyACM0",
settings={},
settings_config=SETTINGS_CONFIG,
status="Ready",
status_type="ok",
raw_json="{}",
)
@app.route("/", methods=["POST"])
def handle_action():
action = request.form.get("action") or ""
device = (request.form.get("device") or "/dev/ttyACM0").strip()
raw_json = (request.form.get("raw_json") or "").strip()
settings = {}
status = "Ready"
status_type = "ok"
if action == "download":
if not device:
flash("Please specify a device.", "error")
status, status_type = "Missing device.", "error"
else:
try:
settings = download_settings(device)
raw_json = json.dumps(settings, indent=2)
flash(f"Settings downloaded from {device}.", "success")
status = f"Settings downloaded from {device}"
except Exception as exc: # pylint: disable=broad-except
if "timeout" in str(exc).lower() or "connection" in str(exc).lower():
flash("Connection timeout. Check device connection.", "error")
status, status_type = "Connection timeout.", "error"
else:
flash(f"Failed to download settings: {exc}", "error")
status, status_type = "Download failed.", "error"
elif action == "upload":
if not device:
flash("Please specify a device.", "error")
status, status_type = "Missing device.", "error"
else:
# Take current form fields as source of truth, falling back to JSON if present
if raw_json:
try:
settings = json.loads(raw_json)
except json.JSONDecodeError:
flash("Raw JSON is invalid; using form values instead.", "error")
settings = {}
form_settings = parse_settings_from_form(request.form)
settings.update(form_settings)
if not settings:
flash("No settings to upload. Download or provide settings first.", "error")
status, status_type = "No settings to upload.", "error"
else:
try:
upload_settings(device, settings)
raw_json = json.dumps(settings, indent=2)
flash(f"Settings uploaded and device reset on {device}.", "success")
status = f"Settings uploaded and device reset on {device}"
except Exception as exc: # pylint: disable=broad-except
if "timeout" in str(exc).lower() or "connection" in str(exc).lower():
flash("Connection timeout. Check device connection.", "error")
status, status_type = "Connection timeout.", "error"
else:
flash(f"Failed to upload settings: {exc}", "error")
status, status_type = "Upload failed.", "error"
elif action == "from_json":
# No-op here, JSON is just edited in the side panel
form_settings = parse_settings_from_form(request.form)
settings.update(form_settings)
if raw_json:
try:
settings.update(json.loads(raw_json))
flash("JSON merged into form values.", "success")
status = "JSON merged into form."
except json.JSONDecodeError:
flash("Invalid JSON; keeping previous form values.", "error")
status, status_type = "JSON parse error.", "error"
elif action == "to_form":
if raw_json:
try:
settings = json.loads(raw_json)
flash("Form fields updated from JSON.", "success")
status = "Form fields updated from JSON."
except json.JSONDecodeError:
flash("Invalid JSON; could not update form fields.", "error")
status, status_type = "JSON parse error.", "error"
elif action == "pretty":
if raw_json:
try:
parsed = json.loads(raw_json)
raw_json = json.dumps(parsed, indent=2)
settings = parsed if isinstance(parsed, dict) else {}
flash("JSON pretty-printed.", "success")
status = "JSON pretty-printed."
except json.JSONDecodeError:
flash("Invalid JSON; cannot pretty-print.", "error")
status, status_type = "JSON parse error.", "error"
elif action == "clear":
settings = {}
raw_json = "{}"
flash("Form cleared.", "success")
status = "Form cleared."
else:
# Unknown / initial action: just reflect form values back
settings = parse_settings_from_form(request.form)
if raw_json and not settings:
try:
settings = json.loads(raw_json)
except json.JSONDecodeError:
pass
return render_template_string(
TEMPLATE,
device=device,
settings=settings,
settings_config=SETTINGS_CONFIG,
status=status,
status_type=status_type,
raw_json=raw_json or json.dumps(settings or {}, indent=2),
)
def main() -> None:
# Bind to all interfaces so you can reach it from your LAN:
# python web_app.py
# Then open: http://:5000/
app.run(host="0.0.0.0", port=5000, debug=False)
if __name__ == "__main__":
main()