168 lines
5.6 KiB
Python
168 lines
5.6 KiB
Python
from dataclasses import dataclass, field
|
|
import struct
|
|
import colorsys
|
|
|
|
|
|
@dataclass
|
|
class ColorRGBW:
|
|
r: float
|
|
g: float
|
|
b: float
|
|
w: float
|
|
|
|
def __repr__(self):
|
|
return f"#({self.r}, {self.g}, {self.b}, {self.w})"
|
|
|
|
def as_bytes(self) -> bytes:
|
|
assert self.is_valid(), "Trying to send invalid " + repr(self)
|
|
return struct.pack("<BBBB", int(self.r * 255), int(self.g * 255), int(self.b * 255),
|
|
int(self.w * 255))
|
|
|
|
def is_valid(self):
|
|
vals = (self.r, self.g, self.b, self.w)
|
|
return all(0 <= v <= 1 for v in vals)
|
|
|
|
def __mul__(self, other:float):
|
|
assert 0<= other <= 1
|
|
return ColorRGBW(self.r * other, self.g * other, self.b * other, self.w * other)
|
|
|
|
def __eq__(self, other:'ColorRGBW'):
|
|
return self.r == other.r and self.g == other.g and self.b == other.b and self.w == other.w
|
|
|
|
def __neq__(self, other:'ColorRGBW'):
|
|
return not self == other
|
|
|
|
def without_white_channel(self, scale=1):
|
|
args = (min(1, e + self.w) for e in (self.r, self.g, self.b) )
|
|
return ColorRGBW(*args, 0)
|
|
|
|
@dataclass
|
|
class ColorHSV:
|
|
h: float
|
|
s: float
|
|
v: float
|
|
|
|
@staticmethod
|
|
def fromRGB(rgb):
|
|
conv = colorsys.rgb_to_hsv(rgb.r, rgb.g, rgb.b)
|
|
return ColorHSV(conv[0] * 360, conv[1], conv[2])
|
|
|
|
def __repr__(self):
|
|
return f"ColorHSV({self.h}, {self.s}, {self.v})"
|
|
|
|
def as_bytes(self) -> bytes:
|
|
return struct.pack("<fff", self.h, self.s, self.v)
|
|
|
|
def is_valid(self):
|
|
if not 0 <= self.h <= 360:
|
|
return False
|
|
if not 0 <= self.s <= 1:
|
|
return False
|
|
if not 0 <= self.v <= 2:
|
|
return False
|
|
return True
|
|
|
|
|
|
@dataclass
|
|
class EffectStaticConfig:
|
|
color: ColorRGBW
|
|
begin: int = 0
|
|
end: int = 0
|
|
|
|
def __repr__(self):
|
|
return f"EffectStaticConfig {str(self.color)}, beg: {self.begin}, end {self.end}"
|
|
|
|
def as_bytes(self) -> bytes:
|
|
return self.color.as_bytes() + struct.pack("<HH", self.begin, self.end)
|
|
|
|
|
|
@dataclass
|
|
class EffectStaticDetailedConfig:
|
|
color: ColorRGBW
|
|
increment: int = 1
|
|
begin: float = 0.0
|
|
end: float = 1.0
|
|
transition_time_in_ms : float = 500
|
|
|
|
def __repr__(self):
|
|
return f"EffectStaticDetailedConfig {str(self.color)}, beg: {self.begin}, end {self.end}, incr {self.increment}, transition in ms {self.transition_time_in_ms}"
|
|
|
|
def as_bytes(self) -> bytes:
|
|
return self.color.as_bytes() + struct.pack("<Hfff", self.increment, self.begin, self.end, self.transition_time_in_ms)
|
|
|
|
@dataclass
|
|
class EffectAlexaSwipeConfig:
|
|
primary_color_width: float = 20 # in degrees
|
|
transition_width: float = 30 # in degrees
|
|
swipe_speed: float = 2 * 360 # in degrees per second
|
|
bell_curve_width_in_leds: float = 3
|
|
start_position: float = 180 # in degrees
|
|
forward: bool = True
|
|
primary_color: ColorRGBW = field(default_factory=lambda: ColorRGBW(0, 0, 1, 0))
|
|
secondary_color: ColorRGBW = field(default_factory=lambda: ColorRGBW(0, 200 / 255, 1, 0))
|
|
|
|
def as_bytes(self) -> bytes:
|
|
return struct.pack(
|
|
"<fffff?", self.primary_color_width, self.transition_width, self.swipe_speed,
|
|
self.bell_curve_width_in_leds, self.start_position,
|
|
self.forward) + self.primary_color.as_bytes() + self.secondary_color.as_bytes()
|
|
|
|
def __repr__(self):
|
|
return f"EffectAlexaSwipe primary {str(self.primary_color)}, {str(self.secondary_color)}"
|
|
|
|
|
|
@dataclass
|
|
class EffectRandomTwoColorInterpolationConfig:
|
|
cycle_durations_ms: int = 6000
|
|
start_with_existing: bool = True
|
|
num_segments: int = 3
|
|
hue1_random: bool = False
|
|
hue2_random: bool = False
|
|
color1: ColorHSV = field(default_factory=lambda: ColorHSV(240, 1, 1))
|
|
color2: ColorHSV = field(default_factory=lambda: ColorHSV(192, 1, 1))
|
|
|
|
def as_bytes(self) -> bytes:
|
|
c1 = ColorHSV.fromRGB(self.color1) if isinstance(self.color1, ColorRGBW) else self.color1
|
|
c2 = ColorHSV.fromRGB(self.color2) if isinstance(self.color2, ColorRGBW) else self.color2
|
|
return struct.pack("<i?i??", self.cycle_durations_ms, self.start_with_existing,
|
|
self.num_segments, self.hue1_random,
|
|
self.hue2_random) + c1.as_bytes() + c2.as_bytes()
|
|
|
|
def __repr__(self):
|
|
return f"RandTwoColor {str(self.color1)}, {str(self.color2)}, segments {self.num_segments}"
|
|
|
|
|
|
@dataclass
|
|
class EffectCircularConfig:
|
|
speed: float = 360 # in degrees per second
|
|
width: float = 180 # in degrees
|
|
color: ColorRGBW = field(default_factory=lambda: ColorRGBW(0, 0, 1, 0))
|
|
|
|
def as_bytes(self) -> bytes:
|
|
return struct.pack("<ff", self.speed, self.width) + self.color.as_bytes()
|
|
|
|
|
|
@dataclass
|
|
class EffectSwipeAndChange:
|
|
swipe: EffectAlexaSwipeConfig = field(default_factory=lambda: EffectAlexaSwipeConfig())
|
|
change: EffectRandomTwoColorInterpolationConfig = field(default_factory=lambda: EffectRandomTwoColorInterpolationConfig())
|
|
|
|
def as_bytes(self) -> bytes:
|
|
return self.swipe.as_bytes() + self.change.as_bytes()
|
|
|
|
def __repr__(self) -> str:
|
|
return f"Swipe and Change: \n {str(self.swipe)}\n {str(self.change)}"
|
|
|
|
|
|
@dataclass
|
|
class EffectReverseSwipe:
|
|
swipeSpeed: float = 2 * 360
|
|
bellCurveWidthInLeds: float = 3
|
|
startPosition: float = 180
|
|
|
|
def as_bytes(self) -> bytes:
|
|
return struct.pack("<fff", self.swipeSpeed, self.bellCurveWidthInLeds, self.startPosition)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"Reverse swipe, speed {self.swipeSpeed}, width in leds {self.bellCurveWidthInLeds}, start position {self.startPosition}"
|