feat(ui): add web led-tool usb controls
Made-with: Cursor
This commit is contained in:
255
src/static/led_tool.js
Normal file
255
src/static/led_tool.js
Normal file
@@ -0,0 +1,255 @@
|
||||
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');
|
||||
|
||||
if (!openBtn || !modal || !form || !portSelect || !outputEl || !messageEl) {
|
||||
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 = '<option value="">Select a serial port</option>';
|
||||
|
||||
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', () => {
|
||||
modal.classList.add('active');
|
||||
loadPorts();
|
||||
});
|
||||
|
||||
if (closeBtn) {
|
||||
closeBtn.addEventListener('click', () => {
|
||||
modal.classList.remove('active');
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user