(function () { const LOG = '[led-tool/editor]'; const log = (...args) => console.log(LOG, ...args); const apiBase = (document.body && document.body.dataset.apiBase) || (window.location.pathname.includes('/led-tool') ? '/led-tool' : ''); const statusEl = document.getElementById('status'); const messageEl = document.getElementById('flash'); const portSelect = document.getElementById('host-port'); const rawJson = document.getElementById('raw_json'); const hostWrap = document.getElementById('host-serial-wrap'); const webWrap = document.getElementById('webserial-wrap'); const connectBtn = document.getElementById('webserial-connect'); const disconnectBtn = document.getElementById('webserial-disconnect'); const hostConnectBtn = document.getElementById('host-connect'); const hostDisconnectBtn = document.getElementById('host-disconnect'); const refreshPortsBtn = document.getElementById('btn-refresh-ports'); const DEFAULT_HOST_PORT = '/dev/ttyACM0'; let webClient = null; let webPort = null; let hostConnected = false; let hostConnectedPort = ''; let transferBusy = false; const webSerialOk = window.LedToolWebSerial && window.LedToolWebSerial.supported(); function setStatus(text, isError) { if (!statusEl) return; statusEl.textContent = text; statusEl.classList.toggle('error', Boolean(isError)); } function flash(text, isError) { if (!messageEl) return; messageEl.textContent = text; messageEl.className = isError ? 'flash error show' : 'flash show'; setTimeout(() => messageEl.classList.remove('show'), 5000); } function formToObject() { const out = {}; document.querySelectorAll('[data-setting]').forEach((el) => { const key = el.getAttribute('data-setting'); if (!key) return; const raw = (el.value || '').trim(); if (raw === '') return; if (el.type === 'number') { const n = parseInt(raw, 10); if (!Number.isNaN(n)) out[key] = n; } else if (el.tagName === 'SELECT' && el.id === 'debug') { out[key] = raw === 'True'; } else { out[key] = raw; } }); return out; } function objectToForm(settings) { if (!settings || typeof settings !== 'object') return; document.querySelectorAll('[data-setting]').forEach((el) => { const key = el.getAttribute('data-setting'); if (!key || !Object.prototype.hasOwnProperty.call(settings, key)) return; const v = settings[key]; if (el.id === 'debug') { el.value = v === true || v === 'True' ? 'True' : 'False'; } else { el.value = v === undefined || v === null ? '' : String(v); } }); if (rawJson) rawJson.value = JSON.stringify(settings, null, 2); } function usingWebSerial() { return webClient && webClient.connected; } function usingHostSerial() { return hostConnected && Boolean(hostConnectedPort); } function updateUi() { const webOpen = usingWebSerial(); const hostOpen = usingHostSerial(); if (webWrap) webWrap.hidden = !webSerialOk || hostOpen; if (connectBtn) connectBtn.hidden = webOpen || hostOpen || !webSerialOk; if (disconnectBtn) disconnectBtn.hidden = !webOpen; if (hostWrap) hostWrap.hidden = webOpen; const hostFieldsLocked = webOpen || hostOpen; if (portSelect) portSelect.disabled = hostFieldsLocked; if (refreshPortsBtn) refreshPortsBtn.disabled = hostFieldsLocked; if (hostConnectBtn) hostConnectBtn.hidden = hostOpen || webOpen; if (hostDisconnectBtn) hostDisconnectBtn.hidden = !hostOpen || webOpen; if (webOpen) { setStatus('USB open (device not reset until Download/Upload).'); } else if (hostOpen) { setStatus(`Host serial: ${hostConnectedPort} (use Download or Upload).`); } else if (webSerialOk) { setStatus('Connect host serial or Web Serial USB, then Download or Upload.'); } else { setStatus(`Connect host serial (default ${DEFAULT_HOST_PORT}).`); } } async function loadHostPorts() { if (!portSelect) return; try { const res = await fetch(`${apiBase}/ports`); const data = await res.json(); const prev = portSelect.value; portSelect.innerHTML = ''; for (const p of data.ports || []) { const opt = document.createElement('option'); opt.value = p.device; opt.textContent = `${p.device} - ${p.description || 'device'}`; portSelect.appendChild(opt); } const acm0 = DEFAULT_HOST_PORT; const hasAcm0 = [...portSelect.options].some((o) => o.value === acm0); if (hasAcm0) { portSelect.value = acm0; } else if (prev && [...portSelect.options].some((o) => o.value === prev)) { portSelect.value = prev; } } catch (e) { flash(`Could not list host ports: ${e.message}`, true); } } function selectedHostPort() { return portSelect && portSelect.value ? portSelect.value.trim() : ''; } function hostPort() { if (hostConnected && hostConnectedPort) return hostConnectedPort; return selectedHostPort(); } function connectHostSerial() { if (usingWebSerial()) { disconnectWebSerial(); } const port = selectedHostPort(); if (!port) { flash('Select a host serial port.', true); return; } hostConnectedPort = port; hostConnected = true; log('connectHostSerial', port); updateUi(); flash(`Host serial ready: ${port}`, false); } function disconnectHostSerial() { hostConnected = false; hostConnectedPort = ''; updateUi(); } async function downloadHost() { const port = hostConnectedPort || hostPort(); if (!port) { flash('Connect host serial first.', true); return; } log('downloadHost', port); setStatus('Downloading from device (host serial)...'); const res = await fetch(`${apiBase}/settings?port=${encodeURIComponent(port)}`); const data = await res.json(); log('downloadHost response', { ok: res.ok, status: res.status, data }); if (!res.ok) { throw new Error(data.error || 'Download failed'); } if (!data.settings) { throw new Error('No settings in response (check CLI output)'); } objectToForm(data.settings); flash('Settings downloaded.', false); setStatus(`Downloaded from ${port}`); } async function uploadHost() { const port = hostConnectedPort || hostPort(); if (!port) { flash('Connect host serial first.', true); return; } const payload = { port, ...formToObject() }; log('uploadHost', payload); setStatus('Uploading via host serial (led-cli)...'); const res = await fetch(`${apiBase}/settings`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Upload failed'); if (!data.ok) throw new Error(data.stderr || 'led-cli failed'); flash('Settings uploaded; device resetting.', false); setStatus('Upload complete.'); } async function connectWebSerial() { if (!window.LedToolWebSerial || !window.LedToolWebSerial.supported()) { flash('Web Serial not supported (use Chrome or Edge over HTTPS or localhost).', true); return; } if (usingHostSerial()) { disconnectHostSerial(); } log('connectWebSerial: requestPort'); setStatus('Opening USB port…'); webPort = await window.LedToolWebSerial.requestPort(); log('connectWebSerial: port selected', webPort); webClient = new window.LedToolWebSerial.MicroPythonRawRepl(); await webClient.connect(webPort); updateUi(); flash('USB port open. Use Download or Upload to talk to the device.', false); } async function disconnectWebSerial() { if (webClient) { try { await webClient.disconnect(); } catch (_) { /* ignore */ } } webClient = null; webPort = null; updateUi(); } async function downloadWebSerial() { log('downloadWebSerial'); setStatus('Reading settings from device (Web Serial)…'); const settings = await webClient.readSettingsJson(); log('downloadWebSerial result', settings); objectToForm(settings); if (rawJson) rawJson.value = JSON.stringify(settings, null, 2); flash('Settings read over Web Serial.', false); setStatus('Downloaded via Web Serial.'); } async function uploadWebSerial() { log('uploadWebSerial'); let settings = {}; if (rawJson && rawJson.value.trim()) { try { settings = JSON.parse(rawJson.value); } catch (e) { flash('Invalid JSON in raw panel.', true); return; } } else { settings = await webClient.readSettingsJson(); Object.assign(settings, formToObject()); } await webClient.writeSettingsJson(settings); flash('Uploaded via Web Serial; device resetting.', false); setStatus('Upload complete.'); await disconnectWebSerial(); } document.getElementById('btn-download')?.addEventListener('click', async () => { if (transferBusy) return; transferBusy = true; try { if (usingWebSerial()) { await downloadWebSerial(); } else if (usingHostSerial()) { await downloadHost(); } else { flash( webSerialOk ? 'Connect host serial or Web Serial USB first.' : 'Click Connect on host serial first.', true, ); } } catch (e) { console.error(LOG, 'download failed', e); if (rawJson) rawJson.value = String(e.message || e); flash(e.message, true); setStatus(e.message, true); } finally { transferBusy = false; } }); document.getElementById('btn-upload')?.addEventListener('click', async () => { if (transferBusy) return; transferBusy = true; try { if (usingWebSerial()) { await uploadWebSerial(); } else if (usingHostSerial()) { await uploadHost(); } else { flash( webSerialOk ? 'Connect host serial or Web Serial USB first.' : 'Click Connect on host serial first.', true, ); } } catch (e) { console.error(LOG, 'upload failed', e); flash(e.message, true); setStatus(e.message, true); } finally { transferBusy = false; } }); document.getElementById('btn-from-json')?.addEventListener('click', () => { try { const parsed = JSON.parse(rawJson.value || '{}'); objectToForm(parsed); flash('Form updated from JSON.', false); } catch (e) { flash('Invalid JSON.', true); } }); document.getElementById('btn-to-json')?.addEventListener('click', () => { const merged = formToObject(); if (rawJson && rawJson.value.trim()) { try { Object.assign(merged, JSON.parse(rawJson.value)); } catch (_) { /* use form only */ } } rawJson.value = JSON.stringify(merged, null, 2); flash('JSON updated from form.', false); }); connectBtn?.addEventListener('click', () => { connectWebSerial().catch((e) => { disconnectWebSerial(); const msg = e.name === 'NotFoundError' ? 'No USB device selected.' : e.name === 'SecurityError' ? 'Web Serial blocked (open LED Tool in a top-level tab, or use host serial on the Pi).' : e.message || String(e); flash(msg, true); setStatus(msg, true); }); }); disconnectBtn?.addEventListener('click', () => { disconnectWebSerial(); flash('Web Serial disconnected.', false); }); refreshPortsBtn?.addEventListener('click', () => loadHostPorts()); hostConnectBtn?.addEventListener('click', () => connectHostSerial()); hostDisconnectBtn?.addEventListener('click', () => { disconnectHostSerial(); flash('Host serial disconnected.', false); }); window.addEventListener('beforeunload', () => { if (webClient) { webClient.disconnect(); } }); log('settings editor ready', { apiBase, webSerialOk }); updateUi(); loadHostPorts(); })();