From 70641c63afbfd98a853ac97d28278cd8da55629d Mon Sep 17 00:00:00 2001 From: Jimmy Date: Mon, 18 May 2026 14:54:18 +1200 Subject: [PATCH] feat(led-tool): embed settings editor in main UI Serve led-tool static editor at /led-tool/editor, filter host serial ports, and load the modal via iframe instead of the legacy form. Co-authored-by: Cursor --- src/controllers/led_tool.py | 54 +++++++- src/static/led_tool.js | 241 +----------------------------------- src/templates/index.html | 79 ++---------- 3 files changed, 60 insertions(+), 314 deletions(-) diff --git a/src/controllers/led_tool.py b/src/controllers/led_tool.py index d39cdfe..a709a2f 100644 --- a/src/controllers/led_tool.py +++ b/src/controllers/led_tool.py @@ -3,20 +3,40 @@ import os import subprocess import sys -from microdot import Microdot +from microdot import Microdot, send_file from serial.tools import list_ports controller = Microdot() +_STATIC_ALLOWED = frozenset( + {"settings_editor.html", "settings_editor.js", "web_serial.js"} +) + def _repo_root() -> str: return os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +def _led_tool_static_dir() -> str: + return os.path.join(_repo_root(), "led-tool", "static") + + def _led_cli_path() -> str: return os.path.join(_repo_root(), "led-tool", "cli.py") +def _filter_host_serial_ports(ports: list) -> list: + mod_path = os.path.join(_repo_root(), "led-tool", "host_ports.py") + if not os.path.isfile(mod_path): + return ports + import importlib.util + + spec = importlib.util.spec_from_file_location("led_tool_host_ports", mod_path) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod.filter_port_dicts(ports) + + def _build_led_cli_command(port: str, payload: dict): cmd = [sys.executable, _led_cli_path(), "--port", port] @@ -92,17 +112,41 @@ def _extract_settings_from_stdout(stdout: str): return None +@controller.get("/editor") +async def settings_editor_page(request): + """led-tool settings UI (Web Serial + host serial via led-cli).""" + path = os.path.join(_led_tool_static_dir(), "settings_editor.html") + if not os.path.isfile(path): + return ( + json.dumps({"error": "led-tool/static/settings_editor.html not found"}), + 404, + {"Content-Type": "application/json"}, + ) + return send_file(path) + + +@controller.get("/static/") +async def led_tool_static(request, filename): + if filename not in _STATIC_ALLOWED: + return "Not found", 404 + path = os.path.join(_led_tool_static_dir(), filename) + if not os.path.isfile(path): + return "Not found", 404 + return send_file(path) + + @controller.get("/ports") async def list_serial_ports(request): - ports = [] - for info in list_ports.comports(): - ports.append( + ports = _filter_host_serial_ports( + [ { "device": info.device, "description": info.description, "hwid": info.hwid, } - ) + for info in list_ports.comports() + ] + ) return ( json.dumps( { diff --git a/src/static/led_tool.js b/src/static/led_tool.js index c3b11b1..3995697 100644 --- a/src/static/led_tool.js +++ b/src/static/led_tool.js @@ -2,254 +2,21 @@ document.addEventListener('DOMContentLoaded', () => { const openBtn = document.getElementById('led-tool-btn'); const modal = document.getElementById('led-tool-modal'); const closeBtn = document.getElementById('led-tool-close-btn'); - const refreshPortsBtn = document.getElementById('led-tool-refresh-ports-btn'); - const form = document.getElementById('led-tool-form'); - const readBtn = document.getElementById('led-tool-read-btn'); - const resetBtn = document.getElementById('led-tool-reset-btn'); - const portSelect = document.getElementById('led-tool-port'); - const outputEl = document.getElementById('led-tool-output'); - const messageEl = document.getElementById('led-tool-message'); + const iframe = document.getElementById('led-tool-iframe'); - if (!openBtn || !modal || !form || !portSelect || !outputEl || !messageEl) { + if (!openBtn || !modal || !iframe) { return; } - const showMessage = (text, type = 'success') => { - messageEl.textContent = text; - messageEl.className = `message ${type} show`; - }; - - const setOutput = (text) => { - outputEl.value = text || ''; - }; - - const parseApiResponse = async (response) => { - const bodyText = await response.text(); - let data = null; - try { - data = bodyText ? JSON.parse(bodyText) : {}; - } catch (error) { - data = { error: bodyText || `HTTP ${response.status}` }; - } - return data; - }; - - const setFieldValue = (id, value) => { - const el = document.getElementById(id); - if (!el) return; - if (value === undefined || value === null) return; - el.value = String(value); - }; - - const populateFormFromSettings = (settings) => { - if (!settings || typeof settings !== 'object') return false; - setFieldValue('led-tool-name', settings.name); - setFieldValue('led-tool-num-leds', settings.num_leds); - setFieldValue('led-tool-led-pin', settings.led_pin); - setFieldValue('led-tool-brightness', settings.brightness); - setFieldValue('led-tool-transport', settings.transport_type); - setFieldValue('led-tool-ssid', settings.ssid); - setFieldValue('led-tool-password', settings.password); - setFieldValue('led-tool-wifi-channel', settings.wifi_channel); - setFieldValue('led-tool-default', settings.default); - return true; - }; - - const loadPorts = async () => { - const defaultPort = '/dev/ttyACM0'; - try { - const response = await fetch('/led-tool/ports'); - const data = await response.json(); - const previous = portSelect.value; - portSelect.innerHTML = ''; - - for (const port of data.ports || []) { - const option = document.createElement('option'); - option.value = port.device; - option.textContent = `${port.device} - ${port.description || 'Unknown'}`; - portSelect.appendChild(option); - } - if (previous) { - portSelect.value = previous; - } else if ((data.ports || []).some((p) => p.device === defaultPort)) { - portSelect.value = defaultPort; - } else { - const fallback = document.createElement('option'); - fallback.value = defaultPort; - fallback.textContent = `${defaultPort} - default`; - portSelect.appendChild(fallback); - portSelect.value = defaultPort; - } - - if (!data.led_cli_exists) { - showMessage('led-tool/cli.py was not found on the host.', 'error'); - } else if ((data.ports || []).length === 0) { - showMessage('No serial ports found.', 'error'); - } else { - showMessage(`Found ${(data.ports || []).length} serial port(s).`, 'success'); - } - } catch (error) { - showMessage(`Failed to read serial ports: ${error.message}`, 'error'); - } - }; - openBtn.addEventListener('click', () => { + iframe.src = '/led-tool/editor'; modal.classList.add('active'); - loadPorts(); }); if (closeBtn) { closeBtn.addEventListener('click', () => { modal.classList.remove('active'); + iframe.src = 'about:blank'; }); } - - if (refreshPortsBtn) { - refreshPortsBtn.addEventListener('click', () => { - loadPorts(); - }); - } - - if (readBtn) { - readBtn.addEventListener('click', async () => { - const port = portSelect.value.trim(); - if (!port) { - showMessage('Select a serial port first.', 'error'); - return; - } - setOutput('Reading settings from device...'); - showMessage('Reading settings over USB...', 'success'); - try { - const response = await fetch(`/led-tool/settings?port=${encodeURIComponent(port)}`); - const data = await parseApiResponse(response); - if (!response.ok) { - showMessage(data.error || 'Read failed.', 'error'); - setOutput(data.error || 'Request failed.'); - return; - } - const output = [ - `exit code: ${data.returncode}`, - '', - 'stdout:', - data.stdout || '(none)', - '', - 'stderr:', - data.stderr || '(none)', - ].join('\n'); - setOutput(output); - if (data.ok) { - const populated = populateFormFromSettings(data.settings); - if (populated) { - showMessage('Settings read and fields populated.', 'success'); - } else { - showMessage('Settings read successfully.', 'success'); - } - } else { - showMessage('Read completed with errors. Check output.', 'error'); - } - } catch (error) { - showMessage(`Request failed: ${error.message}`, 'error'); - setOutput(error.message); - } - }); - } - - if (resetBtn) { - resetBtn.addEventListener('click', async () => { - const port = portSelect.value.trim(); - if (!port) { - showMessage('Select a serial port first.', 'error'); - return; - } - setOutput('Resetting device and following output...'); - showMessage('Resetting device over USB...', 'success'); - try { - const response = await fetch('/led-tool/reset', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ port }), - }); - const data = await parseApiResponse(response); - if (!response.ok) { - showMessage(data.error || 'Reset failed.', 'error'); - setOutput(data.error || 'Request failed.'); - return; - } - const output = [ - `exit code: ${data.returncode}`, - '', - 'stdout:', - data.stdout || '(none)', - '', - 'stderr:', - data.stderr || '(none)', - ].join('\n'); - setOutput(output); - if (data.ok) { - showMessage('Device reset complete.', 'success'); - } else { - showMessage('Reset completed with errors. Check output.', 'error'); - } - } catch (error) { - showMessage(`Request failed: ${error.message}`, 'error'); - setOutput(error.message); - } - }); - } - - form.addEventListener('submit', async (event) => { - event.preventDefault(); - const port = portSelect.value.trim(); - if (!port) { - showMessage('Select a serial port first.', 'error'); - return; - } - - const payload = { - port, - name: document.getElementById('led-tool-name')?.value?.trim() || '', - num_leds: document.getElementById('led-tool-num-leds')?.value?.trim() || '', - led_pin: document.getElementById('led-tool-led-pin')?.value?.trim() || '', - brightness: document.getElementById('led-tool-brightness')?.value?.trim() || '', - transport: document.getElementById('led-tool-transport')?.value?.trim() || '', - ssid: document.getElementById('led-tool-ssid')?.value?.trim() || '', - password: document.getElementById('led-tool-password')?.value?.trim() || '', - wifi_channel: document.getElementById('led-tool-wifi-channel')?.value?.trim() || '', - default: document.getElementById('led-tool-default')?.value?.trim() || '', - }; - - setOutput('Running led-tool command...'); - showMessage('Running command over USB...', 'success'); - try { - const response = await fetch('/led-tool/settings', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), - }); - const data = await parseApiResponse(response); - if (!response.ok) { - showMessage(data.error || 'Command failed.', 'error'); - setOutput(data.error || 'Request failed.'); - return; - } - const output = [ - `exit code: ${data.returncode}`, - '', - 'stdout:', - data.stdout || '(none)', - '', - 'stderr:', - data.stderr || '(none)', - ].join('\n'); - setOutput(output); - if (data.ok) { - showMessage('Settings applied via USB.', 'success'); - } else { - showMessage('Command completed with errors. Check output.', 'error'); - } - } catch (error) { - showMessage(`Request failed: ${error.message}`, 'error'); - setOutput(error.message); - } - }); }); diff --git a/src/templates/index.html b/src/templates/index.html index 4e0c237..9559514 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -765,79 +765,14 @@ - +