kmk_firmware/kmk/rgb.py
2019-07-24 23:03:13 -07:00

492 lines
13 KiB
Python

import time
from math import e, exp, pi, sin
from micropython import const
rgb_config = {
'pixels': None,
'num_pixels': 0,
'pixel_pin': None,
'val_limit': 255,
'hue_default': 0,
'sat_default': 100,
'rgb_order': (1, 0, 2), # GRB WS2812
'val_default': 100,
'hue_step': 1,
'sat_step': 1,
'val_step': 1,
'animation_speed': 1,
'breathe_center': 1.5, # 1.0-2.7
'knight_effect_length': 3,
'animation_mode': 'static',
}
class RGB:
hue = 0
sat = 100
val = 80
pos = 0
time = int(time.monotonic() * 10)
intervals = (30, 20, 10, 5)
animation_speed = 1
enabled = True
neopixel = None
rgbw = False
reverse_animation = False
disable_auto_write = False
animation_mode = 'static'
# Set by config
num_pixels = None
hue_step = None
sat_step = None
val_step = None
breathe_center = None # 1.0-2.7
knight_effect_length = None
val_limit = None
effect_init = False
user_animation = None
def __init__(self, config, pixel_pin):
try:
import neopixel
self.neopixel = neopixel.NeoPixel(
pixel_pin,
config['num_pixels'],
pixel_order=config['rgb_order'],
auto_write=False,
)
if len(config['rgb_order']) == 4:
self.rgbw = True
self.num_pixels = const(config['num_pixels'])
self.hue_step = const(config['hue_step'])
self.sat_step = const(config['sat_step'])
self.val_step = const(config['val_step'])
self.hue = const(config['hue_default'])
self.sat = const(config['sat_default'])
self.val = const(config['val_default'])
self.breathe_center = const(config['breathe_center'])
self.knight_effect_length = const(config['knight_effect_length'])
self.val_limit = const(config['val_limit'])
self.animation_mode = config['animation_mode']
self.animation_speed = const(config['animation_speed'])
if 'user_animation' in config:
print(config['user_animation'])
self.user_animation = config['user_animation']
except ImportError as e:
print(e)
def __repr__(self):
return 'RGB({})'.format(self._to_dict())
def _to_dict(self):
return {
'hue': self.hue,
'sat': self.sat,
'val': self.val,
'time': self.time,
'intervals': self.intervals,
'animation_mode': self.animation_mode,
'animation_speed': self.animation_speed,
'enabled': self.enabled,
'neopixel': self.neopixel,
'disable_auto_write': self.disable_auto_write,
}
def time_ms(self):
return int(time.monotonic() * 1000)
def hsv_to_rgb(self, hue, sat, val):
'''
Converts HSV values, and returns a tuple of RGB values
:param hue:
:param sat:
:param val:
:return: (r, g, b)
'''
r = 0
g = 0
b = 0
if val > self.val_limit:
val = self.val_limit
if sat == 0:
r = val
g = val
b = val
else:
base = ((100 - sat) * val) / 100
color = int((val - base) * ((hue % 60) / 60))
x = int(hue / 60)
if x == 0:
r = val
g = base + color
b = base
elif x == 1:
r = val - color
g = val
b = base
elif x == 2:
r = base
g = val
b = base + color
elif x == 3:
r = base
g = val - color
b = val
elif x == 4:
r = base + color
g = base
b = val
elif x == 5:
r = val
g = base
b = val - color
return int(r), int(g), int(b)
def hsv_to_rgbw(self, 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 = self.hsv_to_rgb(hue, sat, val)
return rgb[0], rgb[1], rgb[2], min(rgb)
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
'''
if self.neopixel:
if self.rgbw:
self.set_rgb(self.hsv_to_rgbw(hue, sat, val), index)
else:
self.set_rgb(self.hsv_to_rgb(hue, sat, val), index)
return self
def set_hsv_fill(self, hue, sat, val):
'''
Takes HSV values and displays it on all LEDs/Neopixels
:param hue:
:param sat:
:param val:
'''
if self.neopixel:
if self.rgbw:
self.set_rgb_fill(self.hsv_to_rgbw(hue, sat, val))
else:
self.set_rgb_fill(self.hsv_to_rgb(hue, sat, val))
return self
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 self.neopixel and 0 <= index <= self.num_pixels - 1:
self.neopixel[index] = rgb
if not self.disable_auto_write:
self.neopixel.show()
return self
def set_rgb_fill(self, rgb):
'''
Takes an RGB or RGBW and displays it on all LEDs/Neopixels
:param rgb: RGB or RGBW
'''
if self.neopixel:
self.neopixel.fill(rgb)
if not self.disable_auto_write:
self.neopixel.show()
return self
def increase_hue(self, step=None):
'''
Increases hue by step amount rolling at 360 and returning to 0
:param step:
'''
if not step:
step = self.hue_step
self.hue = (self.hue + step) % 360
if self._check_update():
self._do_update()
return self
def decrease_hue(self, step=None):
'''
Decreases hue by step amount rolling at 0 and returning to 360
:param step:
'''
if not step:
step = self.hue_step
if (self.hue - step) <= 0:
self.hue = (self.hue + 360 - step) % 360
else:
self.hue = (self.hue - step) % 360
if self._check_update():
self._do_update()
return self
def increase_sat(self, step=None):
'''
Increases saturation by step amount stopping at 100
:param step:
'''
if not step:
step = self.sat_step
if self.sat + step >= 100:
self.sat = 100
else:
self.sat += step
if self._check_update():
self._do_update()
return self
def decrease_sat(self, step=None):
'''
Decreases saturation by step amount stopping at 0
:param step:
'''
if not step:
step = self.sat_step
if (self.sat - step) <= 0:
self.sat = 0
else:
self.sat -= step
if self._check_update():
self._do_update()
return self
def increase_val(self, step=None):
'''
Increases value by step amount stopping at 100
:param step:
'''
if not step:
step = self.val_step
if (self.val + step) >= 100:
self.val = 100
else:
self.val += step
if self._check_update():
self._do_update()
return self
def decrease_val(self, step=None):
'''
Decreases value by step amount stopping at 0
:param step:
'''
if not step:
step = self.val_step
if (self.val - step) <= 0:
self.val = 0
else:
self.val -= step
if self._check_update():
self._do_update()
return self
def increase_ani(self):
'''
Increases animation speed by 1 amount stopping at 10
:param step:
'''
if (self.animation_speed + 1) >= 10:
self.animation_speed = 10
else:
self.val += 1
def decrease_ani(self):
'''
Decreases animation speed by 1 amount stopping at 0
:param step:
'''
if (self.val - 1) <= 0:
self.val = 0
else:
self.val -= 1
return self
def off(self):
'''
Turns off all LEDs/Neopixels without changing stored values
'''
if self.neopixel:
self.set_hsv_fill(0, 0, 0)
return self
def show(self):
'''
Turns on all LEDs/Neopixels without changing stored values
'''
if self.neopixel:
self.neopixel.show()
return self
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.enabled:
if self.animation_mode == 'breathing':
return self.effect_breathing()
elif self.animation_mode == 'rainbow':
return self.effect_rainbow()
elif self.animation_mode == 'breathing_rainbow':
return self.effect_breathing_rainbow()
elif self.animation_mode == 'static':
return self.effect_static()
elif self.animation_mode == 'knight':
return self.effect_knight()
elif self.animation_mode == 'swirl':
return self.effect_swirl()
elif self.animation_mode == 'user':
return self.user_animation(self)
elif self.animation_mode == 'static_standby':
pass
else:
self.off()
return self
def _animation_step(self):
interval = self.time_ms() - self.time
if interval >= max(self.intervals):
self.time = self.time_ms()
return max(self.intervals)
if interval in self.intervals:
return interval
else:
return False
def _init_effect(self):
if (
self.animation_mode == 'breathing'
or self.animation_mode == 'breathing_rainbow'
):
self.intervals = (30, 20, 10, 5)
elif self.animation_mode == 'swirl':
self.intervals = (50, 50)
self.pos = 0
self.reverse_animation = False
self.effect_init = False
return self
def _check_update(self):
if self.animation_mode == 'static_standby':
return True
def _do_update(self):
if self.animation_mode == 'static_standby':
self.animation_mode = 'static'
def effect_static(self):
self.set_hsv_fill(self.hue, self.sat, self.val)
self.animation_mode = 'static_standby'
return self
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.animation_speed) % 256
self.set_hsv_fill(self.hue, self.sat, self.val)
return self
def effect_breathing_rainbow(self):
self.increase_hue(self.animation_speed)
self.effect_breathing()
return self
def effect_rainbow(self):
self.increase_hue(self.animation_speed)
self.set_hsv_fill(self.hue, self.sat, self.val)
return self
def effect_swirl(self):
self.increase_hue(self.animation_speed * 2)
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)) % 360, self.sat, self.val, i
)
# Show final results
self.disable_auto_write = False # Resume showing changes
self.show()
return self
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.animation_speed / 2
else:
self.pos += self.animation_speed / 2
# Show final results
self.disable_auto_write = False # Resume showing changes
self.show()
return self