From cf1d831b5a3fe89f0767a7ef34940c57b37f614b Mon Sep 17 00:00:00 2001 From: jimmy Date: Thu, 29 Jan 2026 00:04:23 +1300 Subject: [PATCH] Align controller backend and data with new presets Update palettes, profiles, tabs, preset sending, and ESPNow message format to match the new preset defaults and driver short-field schema. --- db/palette.json | 47 +++--------- db/preset.json | 150 ++++++++++++++++++++++++++++++++++++- db/profile.json | 12 ++- db/tab.json | 31 +++++++- src/controllers/palette.py | 30 +++++--- src/controllers/preset.py | 6 +- src/models/pallet.py | 20 +++-- src/models/profile.py | 30 +++++++- src/static/presets.js | 3 + src/static/tabs.js | 30 ++++++++ src/util/espnow_message.py | 13 ++-- 11 files changed, 305 insertions(+), 67 deletions(-) diff --git a/db/palette.json b/db/palette.json index 8fa9767..b71dcd5 100644 --- a/db/palette.json +++ b/db/palette.json @@ -1,39 +1,12 @@ { - "1": { - "name": "Default Colors", - "colors": [ - "#FF0000", - "#00FF00", - "#0000FF", - "#FFFF00", - "#FF00FF", - "#00FFFF", - "#FFFFFF", - "#000000", - "#FFA500", - "#800080" - ] - }, - "2": { - "name": "Warm Colors", - "colors": [ - "#FF6B6B", - "#FF8E53", - "#FFA07A", - "#FFD700", - "#FFA500", - "#FF6347" - ] - }, - "3": { - "name": "Cool Colors", - "colors": [ - "#4ECDC4", - "#44A08D", - "#96CEB4", - "#A8E6CF", - "#5F9EA0", - "#4682B4" - ] - } + "1": [ + "#FF0000", + "#00FF00", + "#0000FF", + "#FFFF00", + "#FF00FF", + "#00FFFF", + "#FFFFFF", + "#000000" + ] } diff --git a/db/preset.json b/db/preset.json index 5b517e3..7eef11c 100644 --- a/db/preset.json +++ b/db/preset.json @@ -1 +1,149 @@ -{"1": {"name": "Warm White", "pattern": "on", "colors": ["#FFE5B4", "#FFDAB9", "#FFE4B5"], "brightness": 200, "delay": 100, "n1": 10, "n2": 10, "n3": 10, "n4": 10, "n5": 0, "n6": 0, "n7": 0, "n8": 0}, "2": {"name": "Rainbow", "pattern": "rainbow", "colors": ["#FF0000", "#FF7F00", "#FFFF00", "#00FF00", "#0000FF", "#4B0082", "#9400D3"], "brightness": 255, "delay": 50, "n1": 20, "n2": 15, "n3": 10, "n4": 5, "n5": 0, "n6": 0, "Step Rate": 20, "n7": 0, "n8": 0}, "3": {"name": "Pulse Red", "pattern": "pulse", "colors": ["#FF0000", "#CC0000", "#990000"], "brightness": 180, "delay": 200, "n1": 30, "n2": 20, "n3": 10, "n4": 5, "n5": 0, "n6": 0}} \ No newline at end of file +{ + "1": { + "name": "on", + "pattern": "on", + "colors": [ + "#FFFFFF" + ], + "brightness": 255, + "delay": 100, + "auto": true, + "n1": 0, + "n2": 0, + "n3": 0, + "n4": 0, + "n5": 0, + "n6": 0, + "n7": 0, + "n8": 0 + }, + "2": { + "name": "off", + "pattern": "off", + "colors": [], + "brightness": 0, + "delay": 100, + "auto": true, + "n1": 0, + "n2": 0, + "n3": 0, + "n4": 0, + "n5": 0, + "n6": 0, + "n7": 0, + "n8": 0 + }, + "3": { + "name": "rainbow", + "pattern": "rainbow", + "colors": [], + "brightness": 255, + "delay": 100, + "auto": true, + "n1": 2, + "n2": 0, + "n3": 0, + "n4": 0, + "n5": 0, + "n6": 0, + "n7": 0, + "n8": 0 + }, + "4": { + "name": "transition", + "pattern": "transition", + "colors": [ + "#FF0000", + "#00FF00", + "#0000FF" + ], + "brightness": 255, + "delay": 500, + "auto": true, + "n1": 0, + "n2": 0, + "n3": 0, + "n4": 0, + "n5": 0, + "n6": 0, + "n7": 0, + "n8": 0 + }, + "5": { + "name": "chase", + "pattern": "chase", + "colors": [ + "#FF0000", + "#0000FF" + ], + "brightness": 255, + "delay": 200, + "auto": true, + "n1": 5, + "n2": 5, + "n3": 1, + "n4": 1, + "n5": 0, + "n6": 0, + "n7": 0, + "n8": 0 + }, + "6": { + "name": "pulse", + "pattern": "pulse", + "colors": [ + "#00FF00" + ], + "brightness": 255, + "delay": 500, + "auto": true, + "n1": 1000, + "n2": 500, + "n3": 1000, + "n4": 0, + "n5": 0, + "n6": 0, + "n7": 0, + "n8": 0 + }, + "7": { + "name": "circle", + "pattern": "circle", + "colors": [ + "#FFA500", + "#800080" + ], + "brightness": 255, + "delay": 200, + "auto": true, + "n1": 2, + "n2": 10, + "n3": 2, + "n4": 5, + "n5": 0, + "n6": 0, + "n7": 0, + "n8": 0 + }, + "8": { + "name": "blink", + "pattern": "blink", + "colors": [ + "#FF0000", + "#00FF00", + "#0000FF", + "#FFFF00" + ], + "brightness": 255, + "delay": 1000, + "auto": true, + "n1": 0, + "n2": 0, + "n3": 0, + "n4": 0, + "n5": 0, + "n6": 0, + "n7": 0, + "n8": 0 + } +} \ No newline at end of file diff --git a/db/profile.json b/db/profile.json index 55cf42a..a86e463 100644 --- a/db/profile.json +++ b/db/profile.json @@ -1 +1,11 @@ -{"1": {"name": "Default", "tabs": ["1", "2"], "scenes": ["1", "2"], "palette": ["#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF", "#FFFFFF", "#000000"]}, "2": {"name": "test", "type": "tabs", "tabs": ["12", "13"], "scenes": [], "palette": ["#b93c3c", "#3cb961"], "color_palette": ["#b93c3c", "#3cb961"]}} \ No newline at end of file +{ + "1": { + "name": "default", + "type": "tabs", + "tabs": [ + "1" + ], + "scenes": [], + "palette_id": "1" + } +} \ No newline at end of file diff --git a/db/tab.json b/db/tab.json index 6e19763..4c1e8d9 100644 --- a/db/tab.json +++ b/db/tab.json @@ -1 +1,30 @@ -{"1": {"name": "Main", "names": ["1", "2", "3"], "presets": [["1", "2", "3"]], "presets_flat": ["1", "2", "3"]}, "2": {"name": "Accent", "names": ["4", "5"], "presets": []}, "3": {"name": "", "names": [], "presets": []}, "4": {"name": "", "names": [], "presets": []}, "5": {"name": "", "names": [], "presets": []}, "6": {"name": "", "names": [], "presets": []}, "7": {"name": "", "names": [], "presets": []}, "8": {"name": "", "names": [], "presets": []}, "9": {"name": "", "names": [], "presets": []}, "10": {"name": "", "names": [], "presets": []}, "11": {"name": "", "names": [], "presets": []}, "12": {"name": "test2", "names": ["1"], "presets": [], "colors": ["#b93c3c", "#761e1e", "#ffffff"]}, "13": {"name": "test5", "names": ["1"], "presets": []}} \ No newline at end of file +{ + "1": { + "name": "default", + "names": [ + "1" + ], + "presets": [ + [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8" + ] + ], + "presets_flat": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8" + ] + } +} \ No newline at end of file diff --git a/src/controllers/palette.py b/src/controllers/palette.py index 2d11b76..fd1d9c7 100644 --- a/src/controllers/palette.py +++ b/src/controllers/palette.py @@ -8,14 +8,18 @@ palettes = Palette() @controller.get('') async def list_palettes(request): """List all palettes.""" - return json.dumps(palettes), 200, {'Content-Type': 'application/json'} + data = {} + for pid in palettes.list(): + colors = palettes.read(pid) + data[pid] = colors + return json.dumps(data), 200, {'Content-Type': 'application/json'} @controller.get('/') async def get_palette(request, id): """Get a specific palette by ID.""" palette = palettes.read(id) if palette: - return json.dumps(palette), 200, {'Content-Type': 'application/json'} + return json.dumps({"colors": palette, "id": str(id)}), 200, {'Content-Type': 'application/json'} return json.dumps({"error": "Palette not found"}), 404 @controller.post('') @@ -23,12 +27,14 @@ async def create_palette(request): """Create a new palette.""" try: data = request.json or {} - name = data.get("name", "") colors = data.get("colors", None) - palette_id = palettes.create(name, colors) - if data: - palettes.update(palette_id, data) - return json.dumps(palettes.read(palette_id)), 201, {'Content-Type': 'application/json'} + # Palette no longer needs a name; only colors are stored. + palette_id = palettes.create("", colors) + palette = palettes.read(palette_id) or {} + # Include the ID in the response payload so clients can link it. + palette_with_id = {"id": str(palette_id)} + palette_with_id.update(palette) + return json.dumps(palette_with_id), 201, {'Content-Type': 'application/json'} except Exception as e: return json.dumps({"error": str(e)}), 400 @@ -36,9 +42,15 @@ async def create_palette(request): async def update_palette(request, id): """Update an existing palette.""" try: - data = request.json + data = request.json or {} + # Ignore any name field; only colors are relevant. + if "name" in data: + data.pop("name", None) if palettes.update(id, data): - return json.dumps(palettes.read(id)), 200, {'Content-Type': 'application/json'} + palette = palettes.read(id) or {} + palette_with_id = {"id": str(id)} + palette_with_id.update(palette) + return json.dumps(palette_with_id), 200, {'Content-Type': 'application/json'} return json.dumps({"error": "Palette not found"}), 404 except Exception as e: return json.dumps({"error": str(e)}), 400 diff --git a/src/controllers/preset.py b/src/controllers/preset.py index d6e06d1..68b5784 100644 --- a/src/controllers/preset.py +++ b/src/controllers/preset.py @@ -75,14 +75,14 @@ async def send_presets(request): if not isinstance(preset_ids, list) or not preset_ids: return json.dumps({"error": "preset_ids must be a non-empty list"}), 400, {'Content-Type': 'application/json'} - # Build API-compliant preset map keyed by preset name + # Build API-compliant preset map keyed by preset ID (not name) presets_by_name = {} for pid in preset_ids: preset_data = presets.read(str(pid)) if not preset_data: continue - name_key = preset_data.get('name') or str(pid) - presets_by_name[name_key] = build_preset_dict(preset_data) + preset_id_key = str(pid) + presets_by_name[preset_id_key] = build_preset_dict(preset_data) if not presets_by_name: return json.dumps({"error": "No matching presets found"}), 404, {'Content-Type': 'application/json'} diff --git a/src/models/pallet.py b/src/models/pallet.py index 6eb2cfe..56de198 100644 --- a/src/models/pallet.py +++ b/src/models/pallet.py @@ -6,22 +6,30 @@ class Palette(Model): def create(self, name="", colors=None): next_id = self.get_next_id() - self[next_id] = { - "name": name, - "colors": colors if colors else [] - } + # Store palette as a simple list of colors; name is ignored. + self[next_id] = list(colors) if colors else [] self.save() return next_id def read(self, id): id_str = str(id) - return self.get(id_str, None) + value = self.get(id_str, None) + # Backwards compatibility: if stored as {"colors": [...]}, unwrap. + if isinstance(value, dict) and "colors" in value: + return value.get("colors") or [] + # Otherwise, expect a list of colors. + return value or [] def update(self, id, data): id_str = str(id) if id_str not in self: return False - self[id_str].update(data) + # Accept either {"colors": [...]} or a raw list. + if isinstance(data, dict): + colors = data.get("colors", []) + else: + colors = data + self[id_str] = list(colors) if colors else [] self.save() return True diff --git a/src/models/profile.py b/src/models/profile.py index caec87f..3e91a29 100644 --- a/src/models/profile.py +++ b/src/models/profile.py @@ -1,21 +1,45 @@ from models.model import Model +from models.pallet import Palette + class Profile(Model): def __init__(self): + """Profile model. + + Each profile owns a single, unique palette stored in the Palette model. + The profile stores a `palette_id` that points to its palette; any legacy + inline `palette` arrays are migrated to a dedicated Palette entry. + """ super().__init__() + self._palette_model = Palette() + + # Migrate legacy inline palettes to separate Palette entries. + changed = False + for pid, pdata in list(self.items()): + if isinstance(pdata, dict): + if "palette" in pdata and "palette_id" not in pdata: + colors = pdata.get("palette") or [] + palette_id = self._palette_model.create(colors=colors) + pdata.pop("palette", None) + pdata["palette_id"] = str(palette_id) + changed = True + if changed: + self.save() def create(self, name="", profile_type="tabs"): - """ - Create a new profile. + """Create a new profile and its own empty palette. + profile_type: "tabs" or "scenes" (ignoring scenes for now) """ next_id = self.get_next_id() + # Create a unique palette for this profile. + palette_id = self._palette_model.create(colors=[]) self[next_id] = { "name": name, "type": profile_type, # "tabs" or "scenes" "tabs": [], # Array of tab IDs "scenes": [], # Array of scene IDs (for future use) - "palette": [] + "palette_id": str(palette_id), } self.save() return next_id diff --git a/src/static/presets.js b/src/static/presets.js index 59b5b33..fbdee33 100644 --- a/src/static/presets.js +++ b/src/static/presets.js @@ -1176,6 +1176,9 @@ const sendPresetViaEspNow = (presetId, preset, deviceNames) => { // Expose for other scripts (tabs.js) so they can reuse the shared WebSocket. try { window.sendPresetViaEspNow = sendPresetViaEspNow; + // Expose a generic ESPNow sender so other scripts (tabs.js) can send + // non-preset messages such as global brightness. + window.sendEspnowRaw = sendEspnowMessage; } catch (e) { // window may not exist in some environments; ignore. } diff --git a/src/static/tabs.js b/src/static/tabs.js index 544b5ec..9f609d0 100644 --- a/src/static/tabs.js +++ b/src/static/tabs.js @@ -262,6 +262,11 @@ async function loadTabContent(tabId) {
+
+ + + 255 +
@@ -276,6 +281,31 @@ async function loadTabContent(tabId) { sendTabPresets(tabId); }); } + + // Wire up per-tab brightness slider to send global brightness via ESPNow. + const brightnessSlider = container.querySelector('#tab-brightness-slider'); + const brightnessValue = container.querySelector('#tab-brightness-value'); + // Simple debounce so we don't spam ESPNow while dragging + let brightnessSendTimeout = null; + if (brightnessSlider && brightnessValue) { + brightnessSlider.addEventListener('input', (e) => { + const val = parseInt(e.target.value, 10) || 0; + brightnessValue.textContent = String(val); + if (brightnessSendTimeout) { + clearTimeout(brightnessSendTimeout); + } + brightnessSendTimeout = setTimeout(() => { + if (typeof window.sendEspnowRaw === 'function') { + try { + // Include version so led-driver accepts the message. + window.sendEspnowRaw({ v: '1', b: val }); + } catch (err) { + console.error('Failed to send brightness via ESPNow:', err); + } + } + }, 150); + }); + } // Trigger presets loading if the function exists if (typeof renderTabPresets === 'function') { diff --git a/src/util/espnow_message.py b/src/util/espnow_message.py index fb8b858..c4abe29 100644 --- a/src/util/espnow_message.py +++ b/src/util/espnow_message.py @@ -97,7 +97,7 @@ def build_preset_dict(preset_data): }) """ # Ensure colors are in hex format - colors = preset_data.get("colors", ["#FFFFFF"]) + colors = preset_data.get("colors", preset_data.get("c", ["#FFFFFF"])) if colors: # Convert RGB tuples to hex strings if needed if isinstance(colors[0], list) and len(colors[0]) == 3: @@ -111,12 +111,13 @@ def build_preset_dict(preset_data): else: colors = ["#FFFFFF"] + # Build payload using the short keys expected by led-driver preset = { - "pattern": preset_data.get("pattern", "off"), - "colors": colors, - "delay": preset_data.get("delay", 100), - "brightness": preset_data.get("brightness", preset_data.get("br", 127)), - "auto": preset_data.get("auto", True), + "p": preset_data.get("pattern", preset_data.get("p", "off")), + "c": colors, + "d": preset_data.get("delay", preset_data.get("d", 100)), + "b": preset_data.get("brightness", preset_data.get("b", preset_data.get("br", 127))), + "a": preset_data.get("auto", preset_data.get("a", True)), "n1": preset_data.get("n1", 0), "n2": preset_data.get("n2", 0), "n3": preset_data.get("n3", 0),