#!/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 subprocess import os from pathlib import Path 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 _run_mpremote_copy(from_device: bool, device: str, temp_path: str) -> None: if from_device: cmd = ["mpremote", "connect", device, "cp", ":/settings.json", temp_path] else: cmd = ["mpremote", "connect", device, "cp", temp_path, ":/settings.json"] result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) if result.returncode != 0: raise RuntimeError(f"mpremote error: {result.stderr.strip() or result.stdout.strip()}") def download_settings(device: str) -> dict: """Download settings.json from the device using mpremote.""" temp_file = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) temp_path = temp_file.name temp_file.close() try: _run_mpremote_copy(from_device=True, device=device, temp_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 using mpremote 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() _run_mpremote_copy(from_device=False, device=device, temp_path=temp_path) # Reset device (best effort) try: import serial # type: ignore with serial.Serial(device, baudrate=115200) as ser: ser.write(b"\x03\x03\x04") except Exception: reset_cmd = [ "mpremote", "connect", device, "exec", "import machine; machine.reset()", ] try: subprocess.run(reset_cmd, capture_output=True, text=True, timeout=5) except subprocess.TimeoutExpired: 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

LED Bar Configuration Web Console

Raspberry Pi · MicroPython settings.json live editor
Device: {{ device or "/dev/ttyACM0" }}

Device Connection

Connect to your MicroPython LED controller and sync configuration
{{ status or "Ready" }}
Tip: Download from device → tweak parameters → Upload and reboot.

LED Settings

Edit all fields before uploading back to your controller
{% for field in settings_config %} {% set key, label, field_type = field[0], field[1], field[2] %}
{% if field_type == 'choice' %} {% set choices = field[3] %} {% else %} {% endif %}
{% endfor %}

Raw JSON

For advanced editing, paste or copy the full settings.json
{% 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 subprocess.TimeoutExpired: flash("Connection timeout. Check device connection.", "error") status, status_type = "Connection timeout.", "error" except FileNotFoundError: flash("mpremote not found. Install with: pip install mpremote", "error") status, status_type = "mpremote not found.", "error" except Exception as exc: # pylint: disable=broad-except 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 subprocess.TimeoutExpired: flash("Connection timeout. Check device connection.", "error") status, status_type = "Connection timeout.", "error" except FileNotFoundError: flash("mpremote not found. Install with: pip install mpremote", "error") status, status_type = "mpremote not found.", "error" except Exception as exc: # pylint: disable=broad-except 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()