600 lines
17 KiB
Python
600 lines
17 KiB
Python
|
from adafruit_pixelbuf import PixelBuf
|
||
|
from math import e, exp, pi, sin
|
||
|
|
||
|
from kmk.extensions import Extension
|
||
|
from kmk.handlers.stock import passthrough as handler_passthrough
|
||
|
from kmk.keys import make_key
|
||
|
from kmk.scheduler import create_task
|
||
|
from kmk.utils import Debug, clamp
|
||
|
|
||
|
debug = Debug(__name__)
|
||
|
|
||
|
rgb_config = {}
|
||
|
|
||
|
|
||
|
def hsv_to_rgb(hue, sat, val):
|
||
|
'''
|
||
|
Converts HSV values, and returns a tuple of RGB values
|
||
|
:param hue:
|
||
|
:param sat:
|
||
|
:param val:
|
||
|
:return: (r, g, b)
|
||
|
'''
|
||
|
if sat == 0:
|
||
|
return (val, val, val)
|
||
|
|
||
|
hue = 6 * (hue & 0xFF)
|
||
|
frac = hue & 0xFF
|
||
|
sxt = hue >> 8
|
||
|
|
||
|
base = (0xFF - sat) * val
|
||
|
color = (val * sat * frac) >> 8
|
||
|
val <<= 8
|
||
|
|
||
|
if sxt == 0:
|
||
|
r = val
|
||
|
g = base + color
|
||
|
b = base
|
||
|
elif sxt == 1:
|
||
|
r = val - color
|
||
|
g = val
|
||
|
b = base
|
||
|
elif sxt == 2:
|
||
|
r = base
|
||
|
g = val
|
||
|
b = base + color
|
||
|
elif sxt == 3:
|
||
|
r = base
|
||
|
g = val - color
|
||
|
b = val
|
||
|
elif sxt == 4:
|
||
|
r = base + color
|
||
|
g = base
|
||
|
b = val
|
||
|
elif sxt == 5:
|
||
|
r = val
|
||
|
g = base
|
||
|
b = val - color
|
||
|
|
||
|
return (r >> 8), (g >> 8), (b >> 8)
|
||
|
|
||
|
|
||
|
def hsv_to_rgbw(hue, sat, val):
|
||
|
'''
|
||
|
Converts HSV values, and returns a tuple of RGBW values
|
||
|
:param hue:
|
||
|
:param sat:
|
||
|
:param val:
|
||
|
:return: (r, g, b, w)
|
||
|
'''
|
||
|
rgb = hsv_to_rgb(hue, sat, val)
|
||
|
return rgb[0], rgb[1], rgb[2], min(rgb)
|
||
|
|
||
|
|
||
|
class AnimationModes:
|
||
|
OFF = 0
|
||
|
STATIC = 1
|
||
|
STATIC_STANDBY = 2
|
||
|
BREATHING = 3
|
||
|
RAINBOW = 4
|
||
|
BREATHING_RAINBOW = 5
|
||
|
KNIGHT = 6
|
||
|
SWIRL = 7
|
||
|
USER = 8
|
||
|
|
||
|
|
||
|
class RGB(Extension):
|
||
|
pos = 0
|
||
|
|
||
|
def __init__(
|
||
|
self,
|
||
|
pixel_pin,
|
||
|
num_pixels=0,
|
||
|
rgb_order=(1, 0, 2), # GRB WS2812
|
||
|
val_limit=255,
|
||
|
hue_default=0,
|
||
|
sat_default=255,
|
||
|
val_default=255,
|
||
|
hue_step=4,
|
||
|
sat_step=13,
|
||
|
val_step=13,
|
||
|
animation_speed=1,
|
||
|
breathe_center=1, # 1.0-2.7
|
||
|
knight_effect_length=3,
|
||
|
animation_mode=AnimationModes.STATIC,
|
||
|
effect_init=False,
|
||
|
reverse_animation=False,
|
||
|
user_animation=None,
|
||
|
disable_auto_write=False,
|
||
|
pixels=None,
|
||
|
refresh_rate=60,
|
||
|
):
|
||
|
self.pixel_pin = pixel_pin
|
||
|
self.num_pixels = num_pixels
|
||
|
self.rgb_order = rgb_order
|
||
|
self.hue_step = hue_step
|
||
|
self.sat_step = sat_step
|
||
|
self.val_step = val_step
|
||
|
self.hue = hue_default
|
||
|
self.hue_default = hue_default
|
||
|
self.sat = sat_default
|
||
|
self.sat_default = sat_default
|
||
|
self.val = val_default
|
||
|
self.val_default = val_default
|
||
|
self.breathe_center = breathe_center
|
||
|
self.knight_effect_length = knight_effect_length
|
||
|
self.val_limit = val_limit
|
||
|
self.animation_mode = animation_mode
|
||
|
self.animation_speed = animation_speed
|
||
|
self.effect_init = effect_init
|
||
|
self.reverse_animation = reverse_animation
|
||
|
self.user_animation = user_animation
|
||
|
self.disable_auto_write = disable_auto_write
|
||
|
self.pixels = pixels
|
||
|
self.refresh_rate = refresh_rate
|
||
|
|
||
|
self.rgbw = bool(len(rgb_order) == 4)
|
||
|
|
||
|
self._substep = 0
|
||
|
|
||
|
make_key(
|
||
|
names=('RGB_TOG',), on_press=self._rgb_tog, on_release=handler_passthrough
|
||
|
)
|
||
|
make_key(
|
||
|
names=('RGB_HUI',), on_press=self._rgb_hui, on_release=handler_passthrough
|
||
|
)
|
||
|
make_key(
|
||
|
names=('RGB_HUD',), on_press=self._rgb_hud, on_release=handler_passthrough
|
||
|
)
|
||
|
make_key(
|
||
|
names=('RGB_SAI',), on_press=self._rgb_sai, on_release=handler_passthrough
|
||
|
)
|
||
|
make_key(
|
||
|
names=('RGB_SAD',), on_press=self._rgb_sad, on_release=handler_passthrough
|
||
|
)
|
||
|
make_key(
|
||
|
names=('RGB_VAI',), on_press=self._rgb_vai, on_release=handler_passthrough
|
||
|
)
|
||
|
make_key(
|
||
|
names=('RGB_VAD',), on_press=self._rgb_vad, on_release=handler_passthrough
|
||
|
)
|
||
|
make_key(
|
||
|
names=('RGB_ANI',), on_press=self._rgb_ani, on_release=handler_passthrough
|
||
|
)
|
||
|
make_key(
|
||
|
names=('RGB_AND',), on_press=self._rgb_and, on_release=handler_passthrough
|
||
|
)
|
||
|
make_key(
|
||
|
names=('RGB_MODE_PLAIN', 'RGB_M_P'),
|
||
|
on_press=self._rgb_mode_static,
|
||
|
on_release=handler_passthrough,
|
||
|
)
|
||
|
make_key(
|
||
|
names=('RGB_MODE_BREATHE', 'RGB_M_B'),
|
||
|
on_press=self._rgb_mode_breathe,
|
||
|
on_release=handler_passthrough,
|
||
|
)
|
||
|
make_key(
|
||
|
names=('RGB_MODE_RAINBOW', 'RGB_M_R'),
|
||
|
on_press=self._rgb_mode_rainbow,
|
||
|
on_release=handler_passthrough,
|
||
|
)
|
||
|
make_key(
|
||
|
names=('RGB_MODE_BREATHE_RAINBOW', 'RGB_M_BR'),
|
||
|
on_press=self._rgb_mode_breathe_rainbow,
|
||
|
on_release=handler_passthrough,
|
||
|
)
|
||
|
make_key(
|
||
|
names=('RGB_MODE_SWIRL', 'RGB_M_S'),
|
||
|
on_press=self._rgb_mode_swirl,
|
||
|
on_release=handler_passthrough,
|
||
|
)
|
||
|
make_key(
|
||
|
names=('RGB_MODE_KNIGHT', 'RGB_M_K'),
|
||
|
on_press=self._rgb_mode_knight,
|
||
|
on_release=handler_passthrough,
|
||
|
)
|
||
|
make_key(
|
||
|
names=('RGB_RESET', 'RGB_RST'),
|
||
|
on_press=self._rgb_reset,
|
||
|
on_release=handler_passthrough,
|
||
|
)
|
||
|
|
||
|
def on_runtime_enable(self, sandbox):
|
||
|
return
|
||
|
|
||
|
def on_runtime_disable(self, sandbox):
|
||
|
return
|
||
|
|
||
|
def during_bootup(self, sandbox):
|
||
|
if self.pixels is None:
|
||
|
import neopixel
|
||
|
|
||
|
self.pixels = neopixel.NeoPixel(
|
||
|
self.pixel_pin,
|
||
|
self.num_pixels,
|
||
|
pixel_order=self.rgb_order,
|
||
|
auto_write=not self.disable_auto_write,
|
||
|
)
|
||
|
|
||
|
# PixelBuffer are already iterable, can't do the usual `try: iter(...)`
|
||
|
if issubclass(self.pixels.__class__, PixelBuf):
|
||
|
self.pixels = (self.pixels,)
|
||
|
|
||
|
if self.num_pixels == 0:
|
||
|
for pixels in self.pixels:
|
||
|
self.num_pixels += len(pixels)
|
||
|
|
||
|
if debug.enabled:
|
||
|
for n, pixels in enumerate(self.pixels):
|
||
|
debug(f'pixels[{n}] = {pixels.__class__}[{len(pixels)}]')
|
||
|
|
||
|
self._task = create_task(self.animate, period_ms=(1000 // self.refresh_rate))
|
||
|
|
||
|
def before_matrix_scan(self, sandbox):
|
||
|
return
|
||
|
|
||
|
def after_matrix_scan(self, sandbox):
|
||
|
return
|
||
|
|
||
|
def before_hid_send(self, sandbox):
|
||
|
return
|
||
|
|
||
|
def after_hid_send(self, sandbox):
|
||
|
pass
|
||
|
|
||
|
def on_powersave_enable(self, sandbox):
|
||
|
return
|
||
|
|
||
|
def on_powersave_disable(self, sandbox):
|
||
|
self._do_update()
|
||
|
|
||
|
def deinit(self, sandbox):
|
||
|
for pixel in self.pixels:
|
||
|
pixel.deinit()
|
||
|
|
||
|
def set_hsv(self, hue, sat, val, index):
|
||
|
'''
|
||
|
Takes HSV values and displays it on a single LED/Neopixel
|
||
|
:param hue:
|
||
|
:param sat:
|
||
|
:param val:
|
||
|
:param index: Index of LED/Pixel
|
||
|
'''
|
||
|
|
||
|
val = clamp(val, 0, self.val_limit)
|
||
|
|
||
|
if self.rgbw:
|
||
|
self.set_rgb(hsv_to_rgbw(hue, sat, val), index)
|
||
|
else:
|
||
|
self.set_rgb(hsv_to_rgb(hue, sat, val), index)
|
||
|
|
||
|
def set_hsv_fill(self, hue, sat, val):
|
||
|
'''
|
||
|
Takes HSV values and displays it on all LEDs/Neopixels
|
||
|
:param hue:
|
||
|
:param sat:
|
||
|
:param val:
|
||
|
'''
|
||
|
|
||
|
val = clamp(val, 0, self.val_limit)
|
||
|
|
||
|
if self.rgbw:
|
||
|
self.set_rgb_fill(hsv_to_rgbw(hue, sat, val))
|
||
|
else:
|
||
|
self.set_rgb_fill(hsv_to_rgb(hue, sat, val))
|
||
|
|
||
|
def set_rgb(self, rgb, index):
|
||
|
'''
|
||
|
Takes an RGB or RGBW and displays it on a single LED/Neopixel
|
||
|
:param rgb: RGB or RGBW
|
||
|
:param index: Index of LED/Pixel
|
||
|
'''
|
||
|
if 0 <= index <= self.num_pixels - 1:
|
||
|
for pixels in self.pixels:
|
||
|
if index <= (len(pixels) - 1):
|
||
|
pixels[index] = rgb
|
||
|
break
|
||
|
index -= len(pixels)
|
||
|
|
||
|
if not self.disable_auto_write:
|
||
|
pixels.show()
|
||
|
|
||
|
def set_rgb_fill(self, rgb):
|
||
|
'''
|
||
|
Takes an RGB or RGBW and displays it on all LEDs/Neopixels
|
||
|
:param rgb: RGB or RGBW
|
||
|
'''
|
||
|
for pixels in self.pixels:
|
||
|
pixels.fill(rgb)
|
||
|
if not self.disable_auto_write:
|
||
|
pixels.show()
|
||
|
|
||
|
def increase_hue(self, step=None):
|
||
|
'''
|
||
|
Increases hue by step amount rolling at 256 and returning to 0
|
||
|
:param step:
|
||
|
'''
|
||
|
if step is None:
|
||
|
step = self.hue_step
|
||
|
|
||
|
self.hue = (self.hue + step) % 256
|
||
|
|
||
|
if self._check_update():
|
||
|
self._do_update()
|
||
|
|
||
|
def decrease_hue(self, step=None):
|
||
|
'''
|
||
|
Decreases hue by step amount rolling at 0 and returning to 256
|
||
|
:param step:
|
||
|
'''
|
||
|
if step is None:
|
||
|
step = self.hue_step
|
||
|
|
||
|
if (self.hue - step) <= 0:
|
||
|
self.hue = (self.hue + 256 - step) % 256
|
||
|
else:
|
||
|
self.hue = (self.hue - step) % 256
|
||
|
|
||
|
if self._check_update():
|
||
|
self._do_update()
|
||
|
|
||
|
def increase_sat(self, step=None):
|
||
|
'''
|
||
|
Increases saturation by step amount stopping at 255
|
||
|
:param step:
|
||
|
'''
|
||
|
if step is None:
|
||
|
step = self.sat_step
|
||
|
|
||
|
self.sat = clamp(self.sat + step, 0, 255)
|
||
|
|
||
|
if self._check_update():
|
||
|
self._do_update()
|
||
|
|
||
|
def decrease_sat(self, step=None):
|
||
|
'''
|
||
|
Decreases saturation by step amount stopping at 0
|
||
|
:param step:
|
||
|
'''
|
||
|
if step is None:
|
||
|
step = self.sat_step
|
||
|
|
||
|
self.sat = clamp(self.sat - step, 0, 255)
|
||
|
|
||
|
if self._check_update():
|
||
|
self._do_update()
|
||
|
|
||
|
def increase_val(self, step=None):
|
||
|
'''
|
||
|
Increases value by step amount stopping at 100
|
||
|
:param step:
|
||
|
'''
|
||
|
if step is None:
|
||
|
step = self.val_step
|
||
|
|
||
|
self.val = clamp(self.val + step, 0, 255)
|
||
|
|
||
|
if self._check_update():
|
||
|
self._do_update()
|
||
|
|
||
|
def decrease_val(self, step=None):
|
||
|
'''
|
||
|
Decreases value by step amount stopping at 0
|
||
|
:param step:
|
||
|
'''
|
||
|
if step is None:
|
||
|
step = self.val_step
|
||
|
|
||
|
self.val = clamp(self.val - step, 0, 255)
|
||
|
|
||
|
if self._check_update():
|
||
|
self._do_update()
|
||
|
|
||
|
def increase_ani(self):
|
||
|
'''
|
||
|
Increases animation speed by 1 amount stopping at 10
|
||
|
:param step:
|
||
|
'''
|
||
|
self.animation_speed = clamp(self.animation_speed + 1, 0, 10)
|
||
|
|
||
|
if self._check_update():
|
||
|
self._do_update()
|
||
|
|
||
|
def decrease_ani(self):
|
||
|
'''
|
||
|
Decreases animation speed by 1 amount stopping at 0
|
||
|
:param step:
|
||
|
'''
|
||
|
self.animation_speed = clamp(self.animation_speed - 1, 0, 10)
|
||
|
|
||
|
if self._check_update():
|
||
|
self._do_update()
|
||
|
|
||
|
def off(self):
|
||
|
'''
|
||
|
Turns off all LEDs/Neopixels without changing stored values
|
||
|
'''
|
||
|
self.set_hsv_fill(0, 0, 0)
|
||
|
|
||
|
def show(self):
|
||
|
'''
|
||
|
Turns on all LEDs/Neopixels without changing stored values
|
||
|
'''
|
||
|
for pixels in self.pixels:
|
||
|
pixels.show()
|
||
|
|
||
|
def animate(self):
|
||
|
'''
|
||
|
Activates a "step" in the animation based on the active mode
|
||
|
:return: Returns the new state in animation
|
||
|
'''
|
||
|
if self.effect_init:
|
||
|
self._init_effect()
|
||
|
|
||
|
if self.animation_mode is AnimationModes.STATIC_STANDBY:
|
||
|
return
|
||
|
|
||
|
if self.enable:
|
||
|
self._animation_step()
|
||
|
if self.animation_mode == AnimationModes.BREATHING:
|
||
|
self.effect_breathing()
|
||
|
elif self.animation_mode == AnimationModes.RAINBOW:
|
||
|
self.effect_rainbow()
|
||
|
elif self.animation_mode == AnimationModes.BREATHING_RAINBOW:
|
||
|
self.effect_breathing_rainbow()
|
||
|
elif self.animation_mode == AnimationModes.STATIC:
|
||
|
self.effect_static()
|
||
|
elif self.animation_mode == AnimationModes.KNIGHT:
|
||
|
self.effect_knight()
|
||
|
elif self.animation_mode == AnimationModes.SWIRL:
|
||
|
self.effect_swirl()
|
||
|
elif self.animation_mode == AnimationModes.USER:
|
||
|
self.user_animation(self)
|
||
|
elif self.animation_mode == AnimationModes.STATIC_STANDBY:
|
||
|
pass
|
||
|
else:
|
||
|
self.off()
|
||
|
|
||
|
def _animation_step(self):
|
||
|
self._substep += self.animation_speed / 4
|
||
|
self._step = int(self._substep)
|
||
|
self._substep -= self._step
|
||
|
|
||
|
def _init_effect(self):
|
||
|
self.pos = 0
|
||
|
self.reverse_animation = False
|
||
|
self.effect_init = False
|
||
|
|
||
|
def _check_update(self):
|
||
|
return bool(self.animation_mode == AnimationModes.STATIC_STANDBY)
|
||
|
|
||
|
def _do_update(self):
|
||
|
if self.animation_mode == AnimationModes.STATIC_STANDBY:
|
||
|
self.animation_mode = AnimationModes.STATIC
|
||
|
|
||
|
def effect_static(self):
|
||
|
self.set_hsv_fill(self.hue, self.sat, self.val)
|
||
|
self.animation_mode = AnimationModes.STATIC_STANDBY
|
||
|
|
||
|
def effect_breathing(self):
|
||
|
# http://sean.voisen.org/blog/2011/10/breathing-led-with-arduino/
|
||
|
# https://github.com/qmk/qmk_firmware/blob/9f1d781fcb7129a07e671a46461e501e3f1ae59d/quantum/rgblight.c#L806
|
||
|
sined = sin((self.pos / 255.0) * pi)
|
||
|
multip_1 = exp(sined) - self.breathe_center / e
|
||
|
multip_2 = self.val_limit / (e - 1 / e)
|
||
|
|
||
|
self.val = int(multip_1 * multip_2)
|
||
|
self.pos = (self.pos + self._step) % 256
|
||
|
self.set_hsv_fill(self.hue, self.sat, self.val)
|
||
|
|
||
|
def effect_breathing_rainbow(self):
|
||
|
self.increase_hue(self._step)
|
||
|
self.effect_breathing()
|
||
|
|
||
|
def effect_rainbow(self):
|
||
|
self.increase_hue(self._step)
|
||
|
self.set_hsv_fill(self.hue, self.sat, self.val)
|
||
|
|
||
|
def effect_swirl(self):
|
||
|
self.increase_hue(self._step)
|
||
|
self.disable_auto_write = True # Turn off instantly showing
|
||
|
for i in range(0, self.num_pixels):
|
||
|
self.set_hsv(
|
||
|
(self.hue - (i * self.num_pixels)) % 256, self.sat, self.val, i
|
||
|
)
|
||
|
|
||
|
# Show final results
|
||
|
self.disable_auto_write = False # Resume showing changes
|
||
|
self.show()
|
||
|
|
||
|
def effect_knight(self):
|
||
|
# Determine which LEDs should be lit up
|
||
|
self.disable_auto_write = True # Turn off instantly showing
|
||
|
self.off() # Fill all off
|
||
|
pos = int(self.pos)
|
||
|
|
||
|
# Set all pixels on in range of animation length offset by position
|
||
|
for i in range(pos, (pos + self.knight_effect_length)):
|
||
|
self.set_hsv(self.hue, self.sat, self.val, i)
|
||
|
|
||
|
# Reverse animation when a boundary is hit
|
||
|
if pos >= self.num_pixels or pos - 1 < (self.knight_effect_length * -1):
|
||
|
self.reverse_animation = not self.reverse_animation
|
||
|
|
||
|
if self.reverse_animation:
|
||
|
self.pos -= self._step / 2
|
||
|
else:
|
||
|
self.pos += self._step / 2
|
||
|
|
||
|
# Show final results
|
||
|
self.disable_auto_write = False # Resume showing changes
|
||
|
self.show()
|
||
|
|
||
|
def _rgb_tog(self, *args, **kwargs):
|
||
|
if self.animation_mode == AnimationModes.STATIC:
|
||
|
self.animation_mode = AnimationModes.STATIC_STANDBY
|
||
|
self._do_update()
|
||
|
if self.animation_mode == AnimationModes.STATIC_STANDBY:
|
||
|
self.animation_mode = AnimationModes.STATIC
|
||
|
self._do_update()
|
||
|
if self.enable:
|
||
|
self.off()
|
||
|
self.enable = not self.enable
|
||
|
|
||
|
def _rgb_hui(self, *args, **kwargs):
|
||
|
self.increase_hue()
|
||
|
|
||
|
def _rgb_hud(self, *args, **kwargs):
|
||
|
self.decrease_hue()
|
||
|
|
||
|
def _rgb_sai(self, *args, **kwargs):
|
||
|
self.increase_sat()
|
||
|
|
||
|
def _rgb_sad(self, *args, **kwargs):
|
||
|
self.decrease_sat()
|
||
|
|
||
|
def _rgb_vai(self, *args, **kwargs):
|
||
|
self.increase_val()
|
||
|
|
||
|
def _rgb_vad(self, *args, **kwargs):
|
||
|
self.decrease_val()
|
||
|
|
||
|
def _rgb_ani(self, *args, **kwargs):
|
||
|
self.increase_ani()
|
||
|
|
||
|
def _rgb_and(self, *args, **kwargs):
|
||
|
self.decrease_ani()
|
||
|
|
||
|
def _rgb_mode_static(self, *args, **kwargs):
|
||
|
self.effect_init = True
|
||
|
self.animation_mode = AnimationModes.STATIC
|
||
|
|
||
|
def _rgb_mode_breathe(self, *args, **kwargs):
|
||
|
self.effect_init = True
|
||
|
self.animation_mode = AnimationModes.BREATHING
|
||
|
|
||
|
def _rgb_mode_breathe_rainbow(self, *args, **kwargs):
|
||
|
self.effect_init = True
|
||
|
self.animation_mode = AnimationModes.BREATHING_RAINBOW
|
||
|
|
||
|
def _rgb_mode_rainbow(self, *args, **kwargs):
|
||
|
self.effect_init = True
|
||
|
self.animation_mode = AnimationModes.RAINBOW
|
||
|
|
||
|
def _rgb_mode_swirl(self, *args, **kwargs):
|
||
|
self.effect_init = True
|
||
|
self.animation_mode = AnimationModes.SWIRL
|
||
|
|
||
|
def _rgb_mode_knight(self, *args, **kwargs):
|
||
|
self.effect_init = True
|
||
|
self.animation_mode = AnimationModes.KNIGHT
|
||
|
|
||
|
def _rgb_reset(self, *args, **kwargs):
|
||
|
self.hue = self.hue_default
|
||
|
self.sat = self.sat_default
|
||
|
self.val = self.val_default
|
||
|
if self.animation_mode == AnimationModes.STATIC_STANDBY:
|
||
|
self.animation_mode = AnimationModes.STATIC
|
||
|
self._do_update()
|