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.
This commit is contained in:
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
150
db/preset.json
150
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}}
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
@@ -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"]}}
|
||||
{
|
||||
"1": {
|
||||
"name": "default",
|
||||
"type": "tabs",
|
||||
"tabs": [
|
||||
"1"
|
||||
],
|
||||
"scenes": [],
|
||||
"palette_id": "1"
|
||||
}
|
||||
}
|
||||
31
db/tab.json
31
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": []}}
|
||||
{
|
||||
"1": {
|
||||
"name": "default",
|
||||
"names": [
|
||||
"1"
|
||||
],
|
||||
"presets": [
|
||||
[
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8"
|
||||
]
|
||||
],
|
||||
"presets_flat": [
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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('/<id>')
|
||||
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
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
@@ -262,6 +262,11 @@ async function loadTabContent(tabId) {
|
||||
<div class="profiles-actions" style="margin-bottom: 1rem;">
|
||||
<button class="btn btn-primary" id="preset-add-btn-tab">Add Preset</button>
|
||||
<button class="btn btn-secondary" id="send-tab-presets-btn">Send Presets</button>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem; margin-left: auto;">
|
||||
<label for="tab-brightness-slider" style="white-space: nowrap;">Brightness</label>
|
||||
<input type="range" id="tab-brightness-slider" min="0" max="255" value="255">
|
||||
<span id="tab-brightness-value">255</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="presets-list-tab" class="presets-list">
|
||||
<!-- Presets will be loaded here by presets.js -->
|
||||
@@ -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') {
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user