feat(patterns,api): pattern OTA, graceful shutdown, driver delivery updates

- Pattern controller/UI and presets patterns tab for OTA to Wi-Fi drivers
- Device controller extensions; driver_delivery chunk handling
- main: SIGINT/SIGTERM shutdown, TCP/UDP server close coordination
- Submodule led-driver: Wi-Fi default transport, lazy espnow import, dynamic patterns

Made-with: Cursor
This commit is contained in:
pi
2026-04-11 15:10:23 +12:00
parent 7179b6531e
commit e67de6215a
9 changed files with 1031 additions and 67 deletions

View File

@@ -547,32 +547,26 @@ document.addEventListener('DOMContentLoaded', () => {
presetPatternInput.style.backgroundColor = '';
presetPatternInput.style.cursor = '';
}
// Update labels and visibility based on pattern
updatePresetNLabels(patternName);
// Get pattern config to map descriptive names back to n keys
const patternConfig = cachedPatterns && cachedPatterns[patternName];
const nToLabel = {};
if (patternConfig && typeof patternConfig === 'object') {
// Now n keys are keys, labels are values
Object.entries(patternConfig).forEach(([nKey, label]) => {
if (typeof nKey === 'string' && nKey.startsWith('n') && typeof label === 'string') {
nToLabel[nKey] = label;
}
});
}
// Set n values, checking both n keys and descriptive names
for (let i = 1; i <= 8; i++) {
const nKey = `n${i}`;
const inputEl = document.getElementById(`preset-${nKey}-input`);
if (inputEl) {
// First check if preset has n key directly
if (preset[nKey] !== undefined) {
inputEl.value = preset[nKey] || 0;
} else {
// Check if preset has descriptive name (from pattern.json mapping)
const label = nToLabel[nKey];
if (label && preset[label] !== undefined) {
inputEl.value = preset[label] || 0;
@@ -582,6 +576,9 @@ document.addEventListener('DOMContentLoaded', () => {
}
}
}
// After values: show only mapped n params with labels from pattern.json; clear hidden inputs
updatePresetNLabels(patternName);
updatePresetEditorTabActionsVisibility();
};
@@ -774,44 +771,65 @@ document.addEventListener('DOMContentLoaded', () => {
};
const updatePresetNLabels = (patternName) => {
const rawPatternName = String(patternName || '').trim();
const normalizedPatternName = rawPatternName.endsWith('.py')
? rawPatternName.slice(0, -3)
: rawPatternName;
let patternConfig =
(cachedPatterns && cachedPatterns[rawPatternName]) ||
(cachedPatterns && cachedPatterns[normalizedPatternName]) ||
null;
if (!patternConfig && cachedPatterns && typeof cachedPatterns === 'object') {
const lower = normalizedPatternName.toLowerCase();
const matchedKey = Object.keys(cachedPatterns).find(
(k) => String(k).toLowerCase() === lower,
);
if (matchedKey) {
patternConfig = cachedPatterns[matchedKey];
}
}
if (patternConfig && typeof patternConfig === 'object' && patternConfig.data && typeof patternConfig.data === 'object') {
patternConfig = patternConfig.data;
}
if (patternConfig && typeof patternConfig === 'object' && patternConfig.parameter_mappings && typeof patternConfig.parameter_mappings === 'object') {
patternConfig = patternConfig.parameter_mappings;
}
const labels = {};
const visibleNKeys = new Set();
// Initialize all labels with default n1:, n2:, etc.
for (let i = 1; i <= 8; i++) {
labels[`n${i}`] = `n${i}:`;
}
const patternConfig = cachedPatterns && cachedPatterns[patternName];
if (patternConfig && typeof patternConfig === 'object') {
// Now n values are keys and descriptive names are values
Object.entries(patternConfig).forEach(([key, label]) => {
if (typeof key === 'string' && key.startsWith('n') && typeof label === 'string') {
labels[key] = `${label}:`;
visibleNKeys.add(key); // Mark this n key as visible
const text = label.trim();
if (text) {
labels[key] = `${text}:`;
visibleNKeys.add(key);
}
}
});
}
// Update labels and show/hide input groups
for (let i = 1; i <= 8; i++) {
const nKey = `n${i}`;
const labelEl = document.getElementById(`preset-${nKey}-label`);
const inputEl = document.getElementById(`preset-${nKey}-input`);
const groupEl = labelEl ? labelEl.closest('.n-param-group') : null;
const show = visibleNKeys.has(nKey);
const inputEl = document.getElementById(`preset-${nKey}-input`);
if (labelEl) {
labelEl.textContent = labels[nKey];
labelEl.textContent = show ? labels[nKey] : '';
}
// Show or hide the entire group based on whether it has a mapping
if (groupEl) {
if (visibleNKeys.has(nKey)) {
groupEl.style.display = ''; // Show
} else {
groupEl.style.display = 'none'; // Hide
}
groupEl.style.display = show ? '' : 'none';
}
if (inputEl && !show) {
inputEl.value = '0';
}
}
const nGrid = presetEditorModal && presetEditorModal.querySelector('.n-params-grid');
if (nGrid) {
nGrid.style.display = visibleNKeys.size > 0 ? '' : 'none';
}
};
@@ -845,6 +863,7 @@ document.addEventListener('DOMContentLoaded', () => {
editButton.addEventListener('click', async () => {
currentEditId = presetId;
currentEditTabId = null;
await loadPatterns();
const paletteColors = await getCurrentProfilePaletteColors();
const presetForEditor = {
...(preset || {}),