Files
led-controller/src/util/espnow_message.py
Jimmy 96d1e1b5fd feat(ui): pattern modes, bundles, and zone content kind
Add profile/preset/sequence JSON import and export; map preset mode to
wire n6 with a mode dropdown for multi-mode patterns; zone edit shows
presets or sequences only with content_kind on save; update catalogue
and tests for merged pattern names.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-16 21:12:42 +12:00

268 lines
8.5 KiB
Python

"""
Message builder for LED driver API communication.
Builds JSON messages according to the LED driver API specification
for sending presets and select commands over the transport (e.g. serial).
"""
import json
def build_message(presets=None, select=None, save=False, default=None):
"""
Build an API message (presets and/or select) as a JSON string.
Args:
presets: Dictionary mapping preset names to preset objects, or None
select: Dictionary mapping device names to select lists, or None
Returns:
JSON string ready to send over the transport
Example:
message = build_message(
presets={
"red_blink": {
"pattern": "blink",
"colors": ["#FF0000"],
"delay": 200,
"brightness": 255,
"auto": True
}
},
select={
"device1": ["red_blink"]
}
)
"""
message = {
"v": "1"
}
if presets:
message["presets"] = presets
# When sending presets, optionally include a save flag so the
# led-driver can persist them.
if save:
message["save"] = True
if select:
message["select"] = select
if default is not None:
message["default"] = default
return json.dumps(message)
def build_select_message(device_name, preset_name, step=None):
"""
Build a select message for a single device.
Args:
device_name: Name of the device
preset_name: Name of the preset to select
step: Optional step value for synchronization
Returns:
Dictionary with select field ready to use in build_message
Example:
select = build_select_message("device1", "rainbow_preset", step=10)
message = build_message(select=select)
"""
select_list = [preset_name]
if step is not None:
select_list.append(step)
return {device_name: select_list}
def _hex_from_background_raw(bg_raw):
"""Coerce ``background`` / ``bg`` field to a ``#RRGGBB`` string (driver wire format)."""
if isinstance(bg_raw, str):
bg = bg_raw if bg_raw.startswith("#") else f"#{bg_raw}"
return bg
if isinstance(bg_raw, (list, tuple)) and len(bg_raw) == 3:
return f"#{int(bg_raw[0]):02x}{int(bg_raw[1]):02x}{int(bg_raw[2]):02x}"
return "#000000"
def resolve_preset_background_hex(preset_data, palette_colors=None):
"""
Resolved background as ``#RRGGBB``. When ``palette_colors`` is a non-empty list and
``background_palette_ref`` is set, uses that palette index; otherwise stored ``background`` / ``bg``.
"""
if not isinstance(preset_data, dict):
return "#000000"
pal = list(palette_colors) if isinstance(palette_colors, list) else []
ref = preset_data.get("background_palette_ref", preset_data.get("backgroundPaletteRef"))
if pal and ref is not None:
try:
idx = int(ref)
except (TypeError, ValueError):
idx = None
else:
if isinstance(idx, int) and 0 <= idx < len(pal):
c = pal[idx]
if isinstance(c, str) and c.strip().startswith("#"):
s = c.strip()
if len(s) == 7 and all(ch in "0123456789abcdefABCDEF" for ch in s[1:]):
return s.upper()
bg_raw = preset_data.get("background", preset_data.get("bg", "#000000"))
return _hex_from_background_raw(bg_raw)
def wire_n6(preset_data, default=0):
"""Resolve style mode for the wire (``n6``); preset may store ``mode`` or ``n6``."""
if not isinstance(preset_data, dict):
return default
if preset_data.get("mode") is not None:
try:
return max(0, int(preset_data["mode"]))
except (TypeError, ValueError):
pass
try:
return max(0, int(preset_data.get("n6", default) or 0))
except (TypeError, ValueError):
return default
def build_preset_dict(preset_data, palette_colors=None):
"""
Convert preset data to API-compliant format.
Args:
preset_data: Dictionary with preset fields (may include name, pattern, colors, etc.)
palette_colors: Optional list of ``#RRGGBB`` strings for ``background_palette_ref`` resolution.
Returns:
Dictionary with preset in API-compliant format (without name field)
Example:
preset = build_preset_dict({
"name": "red_blink",
"pattern": "blink",
"colors": ["#FF0000"],
"delay": 200,
"brightness": 255,
"auto": True,
"n1": 0,
"n2": 0,
"n3": 0,
"n4": 0,
"n5": 0,
"n6": 0
})
"""
# Ensure colors are in hex format
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:
# RGB tuple format [r, g, b]
colors = [f"#{r:02x}{g:02x}{b:02x}" for r, g, b in colors]
elif not isinstance(colors[0], str):
# Handle other formats - convert to hex
colors = ["#FFFFFF"]
# Ensure all colors start with #
colors = [c if c.startswith("#") else f"#{c}" for c in colors]
else:
colors = ["#FFFFFF"]
def _coerce_auto(raw):
if isinstance(raw, bool):
return raw
if raw is None:
return True
if isinstance(raw, int):
return raw != 0
if isinstance(raw, str):
lowered = raw.strip().lower()
if lowered in ("false", "0", "no", "off"):
return False
if lowered in ("true", "1", "yes", "on"):
return True
return True
auto_raw = preset_data.get("auto", preset_data.get("a", True))
auto_bool = _coerce_auto(auto_raw)
bg = resolve_preset_background_hex(preset_data, palette_colors)
# Build payload using the short keys expected by led-driver
preset = {
"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": auto_bool,
"bg": bg,
"n1": preset_data.get("n1", 0),
"n2": preset_data.get("n2", 0),
"n3": preset_data.get("n3", 0),
"n4": preset_data.get("n4", 0),
"n5": preset_data.get("n5", 0),
"n6": wire_n6(preset_data),
}
return preset
def build_presets_dict(presets_data, palette_colors=None):
"""
Convert multiple presets to API-compliant format.
Args:
presets_data: Dictionary mapping preset names to preset data
palette_colors: Optional list of ``#RRGGBB`` strings for background palette ref resolution.
Returns:
Dictionary mapping preset names to API-compliant preset objects
Example:
presets = build_presets_dict({
"red_blink": {
"pattern": "blink",
"colors": ["#FF0000"],
"delay": 200
},
"blue_pulse": {
"pattern": "pulse",
"colors": ["#0000FF"],
"delay": 100
}
})
"""
result = {}
for preset_name, preset_data in presets_data.items():
result[preset_name] = build_preset_dict(preset_data, palette_colors)
return result
def build_select_dict(device_preset_mapping, step_mapping=None):
"""
Build a select dictionary mapping device names to select lists.
Args:
device_preset_mapping: Dictionary mapping device names to preset names
step_mapping: Optional dictionary mapping device names to step values
Returns:
Dictionary with select field ready to use in build_message
Example:
select = build_select_dict(
{"device1": "rainbow_preset", "device2": "pulse_preset"},
step_mapping={"device1": 10}
)
message = build_message(select=select)
"""
select = {}
for device_name, preset_name in device_preset_mapping.items():
select_list = [preset_name]
if step_mapping and device_name in step_mapping:
select_list.append(step_mapping[device_name])
select[device_name] = select_list
return select