diff --git a/src/controllers/preset.py b/src/controllers/preset.py index 1eac6f6..4c2a3dc 100644 --- a/src/controllers/preset.py +++ b/src/controllers/preset.py @@ -4,6 +4,7 @@ from models.preset import Preset from models.profile import Profile from models.espnow import ESPNow from util.espnow_message import build_message, build_preset_dict +import asyncio import json controller = Microdot() @@ -130,8 +131,9 @@ async def send_presets(request, session): return json.dumps({"error": "preset_ids must be a non-empty list"}), 400, {'Content-Type': 'application/json'} save_flag = data.get('save', True) save_flag = bool(save_flag) + default_id = data.get('default') - # Build API-compliant preset map keyed by preset ID (not name) + # Build API-compliant preset map keyed by preset ID, include name current_profile_id = get_current_profile_id(session) presets_by_name = {} for pid in preset_ids: @@ -140,21 +142,27 @@ async def send_presets(request, session): continue if str(preset_data.get("profile_id")) != str(current_profile_id): continue - preset_id_key = str(pid) - presets_by_name[preset_id_key] = build_preset_dict(preset_data) + preset_key = str(pid) + preset_payload = build_preset_dict(preset_data) + preset_payload["name"] = preset_data.get("name", "") + presets_by_name[preset_key] = preset_payload if not presets_by_name: return json.dumps({"error": "No matching presets found"}), 404, {'Content-Type': 'application/json'} + if default_id is not None and str(default_id) not in presets_by_name: + default_id = None + # Use shared ESPNow singleton esp = ESPNow() async def send_chunk(chunk_presets): # Include save flag so the led-driver can persist when desired. - msg = build_message(presets=chunk_presets, save=save_flag) + msg = build_message(presets=chunk_presets, save=save_flag, default=default_id) await esp.send(msg) MAX_BYTES = 240 + SEND_DELAY_MS = 100 entries = list(presets_by_name.items()) total_presets = len(entries) messages_sent = 0 @@ -164,7 +172,7 @@ async def send_presets(request, session): for name, preset_obj in entries: test_batch = dict(batch) test_batch[name] = preset_obj - test_msg = build_message(presets=test_batch, save=save_flag) + test_msg = build_message(presets=test_batch, save=save_flag, default=default_id) size = len(test_msg) if size <= MAX_BYTES or not batch: @@ -172,12 +180,14 @@ async def send_presets(request, session): last_msg = test_msg else: await send_chunk(batch) + await asyncio.sleep_ms(SEND_DELAY_MS) messages_sent += 1 batch = {name: preset_obj} - last_msg = build_message(presets=batch, save=save_flag) + last_msg = build_message(presets=batch, save=save_flag, default=default_id) if batch: await send_chunk(batch) + await asyncio.sleep_ms(SEND_DELAY_MS) messages_sent += 1 return json.dumps({ diff --git a/src/models/tab.py b/src/models/tab.py index c55ca3a..f29fd1a 100644 --- a/src/models/tab.py +++ b/src/models/tab.py @@ -9,7 +9,8 @@ class Tab(Model): self[next_id] = { "name": name, "names": names if names else [], - "presets": presets if presets else [] + "presets": presets if presets else [], + "default_preset": None } self.save() return next_id diff --git a/src/static/presets.js b/src/static/presets.js index 31387c0..50af1b2 100644 --- a/src/static/presets.js +++ b/src/static/presets.js @@ -96,6 +96,7 @@ document.addEventListener('DOMContentLoaded', () => { const presetAddColorButton = document.getElementById('preset-add-color-btn'); const presetBrightnessInput = document.getElementById('preset-brightness-input'); const presetDelayInput = document.getElementById('preset-delay-input'); + const presetDefaultButton = document.getElementById('preset-default-btn'); const presetSaveButton = document.getElementById('preset-save-btn'); const presetClearButton = document.getElementById('preset-clear-btn'); const presetAddFromPaletteButton = document.getElementById('preset-add-from-palette-btn'); @@ -439,6 +440,38 @@ document.addEventListener('DOMContentLoaded', () => { } }; + const getActiveTabId = () => { + if (currentEditTabId) { + return currentEditTabId; + } + const section = document.querySelector('.presets-section[data-tab-id]'); + return section ? section.dataset.tabId : null; + }; + + const updateTabDefaultPreset = async (presetId) => { + const tabId = getActiveTabId(); + if (!tabId) { + return; + } + try { + const tabResponse = await fetch(`/tabs/${tabId}`, { + headers: { Accept: 'application/json' }, + }); + if (!tabResponse.ok) { + return; + } + const tabData = await tabResponse.json(); + tabData.default_preset = presetId; + await fetch(`/tabs/${tabId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(tabData), + }); + } catch (error) { + console.warn('Failed to save tab default preset:', error); + } + }; + const openEditor = () => { if (presetEditorModal) { presetEditorModal.classList.add('active'); @@ -996,12 +1029,30 @@ document.addEventListener('DOMContentLoaded', () => { // Work out the preset ID: for existing presets use currentEditId, otherwise fall back to name const presetId = currentEditId || payload.name; // First send/override the preset definition under its ID - sendPresetViaEspNow(presetId, payload, null); + sendPresetViaEspNow(presetId, payload, null, true, false); // Then send a separate select-only message for this preset ID to all devices in the tab sendSelectForCurrentTabDevices(presetId, section); }); } + if (presetDefaultButton) { + presetDefaultButton.addEventListener('click', async () => { + const payload = buildPresetPayload(); + if (!payload.name) { + alert('Preset name is required.'); + return; + } + const section = document.querySelector('.presets-section[data-tab-id]'); + const namesAttr = section && section.getAttribute('data-device-names'); + const deviceNames = namesAttr + ? namesAttr.split(',').map((n) => n.trim()).filter((n) => n.length > 0) + : []; + const presetId = currentEditId || payload.name; + await updateTabDefaultPreset(presetId); + sendDefaultPreset(presetId, deviceNames); + }); + } + presetSaveButton.addEventListener('click', async () => { const payload = buildPresetPayload(); if (!payload.name) { @@ -1035,18 +1086,18 @@ document.addEventListener('DOMContentLoaded', () => { if (saved && typeof saved === 'object') { if (currentEditId) { // PUT returns the preset object directly; use the existing ID - sendPresetViaEspNow(currentEditId, saved, deviceNames); + sendPresetViaEspNow(currentEditId, saved, deviceNames, true, false); } else { // POST returns { id: preset } const entries = Object.entries(saved); if (entries.length > 0) { const [newId, presetData] = entries[0]; - sendPresetViaEspNow(newId, presetData, deviceNames); + sendPresetViaEspNow(newId, presetData, deviceNames, true, false); } } } else { // Fallback: send what we just built - sendPresetViaEspNow(payload.name, payload, deviceNames); + sendPresetViaEspNow(payload.name, payload, deviceNames, true, false); } await loadPresets(); @@ -1112,7 +1163,7 @@ document.addEventListener('DOMContentLoaded', () => { // Build an ESPNow preset message for a single preset and optionally include a select // for the given device names, then send it via WebSocket. // saveToDevice defaults to true. -const sendPresetViaEspNow = (presetId, preset, deviceNames, saveToDevice = true) => { +const sendPresetViaEspNow = (presetId, preset, deviceNames, saveToDevice = true, setDefault = false) => { try { const colors = Array.isArray(preset.colors) && preset.colors.length ? preset.colors @@ -1142,6 +1193,9 @@ const sendPresetViaEspNow = (presetId, preset, deviceNames, saveToDevice = true) // Instruct led-driver to save this preset when received. message.save = true; } + if (setDefault) { + message.default = presetId; + } // Optionally include a select section for specific devices if (Array.isArray(deviceNames) && deviceNames.length > 0) { @@ -1163,6 +1217,26 @@ const sendPresetViaEspNow = (presetId, preset, deviceNames, saveToDevice = true) } }; +const sendDefaultPreset = (presetId, deviceNames) => { + if (!presetId) { + alert('Select a preset to set as default.'); + return; + } + const message = { v: '1', default: presetId }; + if (Array.isArray(deviceNames) && deviceNames.length > 0) { + const select = {}; + deviceNames.forEach((name) => { + if (name) { + select[name] = [presetId]; + } + }); + if (Object.keys(select).length > 0) { + message.select = select; + } + } + sendEspnowMessage(message); +}; + // Expose for other scripts (tabs.js) so they can reuse the shared WebSocket. try { window.sendPresetViaEspNow = sendPresetViaEspNow; diff --git a/src/static/tabs.js b/src/static/tabs.js index b012391..1e3b227 100644 --- a/src/static/tabs.js +++ b/src/static/tabs.js @@ -319,7 +319,6 @@ async function loadTabContent(tabId) { const deviceNames = Array.isArray(tab.names) ? tab.names.join(',') : ''; container.innerHTML = `