Squashed commit of the following: commit 03d11a16b3edc1f440251420b1105d20872ebdd7 Merge: e052ab095 ba4f343e9 Author: zkk <1007518571@qq.com> Date: Tue Dec 31 17:28:20 2024 +0800 Merge commit 'ba4f343e9430af5538610529dd400f1f954b0078' into release commit ba4f343e9430af5538610529dd400f1f954b0078 Author: ruipeng <1041589370@qq.com> Date: Tue Dec 31 17:25:28 2024 +0800 F430NX:Y驱动细分由128->64 commit e052ab0957f7991a78b90e9974ae07d60d266226 Merge: 25bc7c6d9 b2b98c057 Author: zkk <1007518571@qq.com> Date: Tue Dec 31 15:15:14 2024 +0800 Merge commit 'b2b98c057a7d23f9c8b0aec4c14844f7c24d6bde' into release commit b2b98c057a7d23f9c8b0aec4c14844f7c24d6bde Author: ruipeng <1041589370@qq.com> Date: Tue Dec 31 10:56:18 2024 +0800 F430NX状态灯功能 commit f3cdc1dd75d96f976502946e752e9f7831288469 Author: ruipeng <1041589370@qq.com> Date: Tue Dec 31 10:55:48 2024 +0800 全系支持PRINT_START,PRINT_END宏 commit ecbde51331316e6c454c09d03d93c3337ae17d8a Author: ruipeng <1041589370@qq.com> Date: Tue Dec 31 10:54:11 2024 +0800 F430NX二喷头静音阈值由200->300 commit 40985b8d9a7a8e26fb3c448f0794dab82d866382 Author: ruipeng <1041589370@qq.com> Date: Tue Dec 31 10:53:55 2024 +0800 f430NX增加驱动器散热风扇控制 commit 682e343d6176c1c0764a7befd12d7971374b2b29 Author: ruipeng <1041589370@qq.com> Date: Mon Dec 30 17:27:29 2024 +0800 修复必须回零后才能取消打印的问题 commit e941884b8ec1d5aec058a6fdc2a2aa94f40bf8b5 Author: ruipeng <1041589370@qq.com> Date: Mon Dec 30 16:29:08 2024 +0800 修复F430NX机型Y轴在零位时回零报错的问题 commit e2a9ff2bfe2f9d4971045ce6fbd6f9835cf6475a Author: ruipeng <1041589370@qq.com> Date: Mon Dec 30 13:36:54 2024 +0800 优化F430NX复制、镜像模式,解决坐标超范围问题 commit 4fe4b7f67dc4b09b67dfb930e3bae80a525e164f Author: ruipeng <1041589370@qq.com> Date: Mon Dec 30 13:32:25 2024 +0800 调整F430NX喷头二X电流,由1->1.5 commit f8f7aabbe226d987bea3ba9033569b12eb00f182 Author: zkk <1007518571@qq.com> Date: Fri Dec 27 17:15:17 2024 +0800 关闭F430NX热风校验功能 commit 7f532dc1019ad4ade1992c835b925f162c80ba1a Author: zkk <1007518571@qq.com> Date: Fri Dec 27 17:14:51 2024 +0800 删除旧的设置offset宏定义 commit 34b6971b41e0b6a7bce978901404cec1e80b17a4 Author: ruipeng <1041589370@qq.com> Date: Fri Dec 27 15:29:04 2024 +0800 修复自动切头开关逻辑,优化恢复打印弹窗内容 commit 765bc8dc22cad7f75f2d8d76f0add94dc0ab802a Author: ruipeng <1041589370@qq.com> Date: Thu Dec 26 11:41:34 2024 +0800 优化耗材检测弹窗内容及格式 commit 25bc7c6d9ee89e6cd4e326bb8497faf6a6256fcd Merge: 588ea4402 9b487abec Author: zkk <1007518571@qq.com> Date: Thu Dec 19 16:23:07 2024 +0800 Merge commit '9b487abec3449dea3673618c1a8b9e88f16b4c40' into release commit 9b487abec3449dea3673618c1a8b9e88f16b4c40 Author: ruipeng <1041589370@qq.com> Date: Thu Dec 19 16:03:04 2024 +0800 F430NX更换为闭环Y电机 commit 641f767175387cacf9820affd616dcdb017590b9 Author: ruipeng <1041589370@qq.com> Date: Thu Dec 19 10:42:11 2024 +0800 F430NX复制、镜像模式优化 commit ef148604da4f925305939e74cfb0edc7fbd261d1 Author: ruipeng <1041589370@qq.com> Date: Thu Dec 19 10:40:25 2024 +0800 自动切头功能 commit fa74a82184caa6a914a32578b31aeb21f8299c7c Merge: 9307b8957 93efb1abd Author: zkk <1007518571@qq.com> Date: Wed Dec 18 15:53:46 2024 +0800 Merge branch 'develop' of https://server.creatbot.com/Gitea/CreatBot/CreatBotKlipper into develop commit 9307b89570a63040259b457ef7ff8969b394b527 Author: zkk <1007518571@qq.com> Date: Wed Dec 18 15:49:41 2024 +0800 修复重启时候有概率出现温度数值异常的问题 commit 9925c7864471f1f9f7b1e22f45dc4d1421c1245a Author: zkk <1007518571@qq.com> Date: Tue Dec 17 15:55:28 2024 +0800 实现二喷头的偏移值校准功能 commit 93efb1abdea67a732b17351b8c31ae5387eaaf91 Author: ruipeng <1041589370@qq.com> Date: Fri Dec 13 09:55:41 2024 +0800 配置网床调平淡出高度 commit 30b77165a7bdae2cd338c283fbab3c8baa37c694 Author: ruipeng <1041589370@qq.com> Date: Thu Dec 12 14:03:30 2024 +0800 调整F430NX探测舵机角度,由90°->120° commit 588ea4402812b09100b2417b040d7daa2d2fbfaa Merge: 349f8f7a2 9be203c58 Author: zkk <1007518571@qq.com> Date: Thu Dec 12 10:39:56 2024 +0800 Merge commit '9be203c58f7c88e4f6963a1079d2dcfaa5401dde' into release commit 9be203c58f7c88e4f6963a1079d2dcfaa5401dde Author: ruipeng <1041589370@qq.com> Date: Wed Dec 11 15:24:05 2024 +0800 新增D600pro2、D1000的V0版机型 commit 349f8f7a249ac113700eed2b4a234473016d32c0 Merge: e554ddb83 401da556f Author: zkk <1007518571@qq.com> Date: Fri Nov 29 10:21:01 2024 +0800 Merge branch 'develop' into release commit 401da556f58df5e917a379dd54565ef5775f7ed2 Merge: 508378825 52ea64cf8 Author: zkk <1007518571@qq.com> Date: Fri Nov 29 10:12:48 2024 +0800 Merge branch 'develop' of https://server.creatbot.com/Gitea/CreatBot/CreatBotKlipper into develop commit 52ea64cf83535c84c378239b43a7f0a6a362a517 Author: ruipeng <1041589370@qq.com> Date: Thu Nov 28 10:55:09 2024 +0800 修复暂停掉温后,恢复时喷头温度不能恢复的问题 commit 508378825da2881043b88427a6df03230c9f66c0 Author: zkk <1007518571@qq.com> Date: Wed Nov 27 14:47:23 2024 +0800 D1000 z轴最大速度设置成7 commit 41c2cd39275dfecafd85a0cbe504f2f5f1cdeec6 Author: zkk <1007518571@qq.com> Date: Mon Nov 25 17:12:23 2024 +0800 优化热电偶最大报错次数 # Conflicts: # config/CreatBot_D1000/base.cfg # config/CreatBot_D1000_V0/base.cfg # config/CreatBot_D600Pro2/base.cfg resolved by 03d11a16b3edc1f440251420b1105d20872ebdd7 version # config/CreatBot_D600Pro2_V0/base.cfg resolved by 03d11a16b3edc1f440251420b1105d20872ebdd7 version # config/CreatBot_F430NX/base.cfg resolved by 03d11a16b3edc1f440251420b1105d20872ebdd7 version
1556 lines
62 KiB
Python
1556 lines
62 KiB
Python
# Support for addressable LED visual effects
|
|
# using neopixel and dotstar LEDs
|
|
#
|
|
# Copyright (C) 2020 Paul McGowan <mental405@gmail.com>
|
|
# co-authored by Julian Schill <j.schill@web.de>
|
|
#
|
|
# This file may be distributed under the terms of the GNU GPLv3 license.
|
|
from math import cos, exp, pi
|
|
from random import randint
|
|
|
|
ANALOG_SAMPLE_TIME = 0.001
|
|
ANALOG_SAMPLE_COUNT = 5
|
|
ANALOG_REPORT_TIME = 0.05
|
|
|
|
COLORS = 4
|
|
|
|
######################################################################
|
|
# Custom color value list, returns lists of [r, g ,b] values
|
|
# from a one dimensional list
|
|
######################################################################
|
|
|
|
class colorArray(list):
|
|
def __init__(self, num_colors, kwargs):
|
|
self.n=num_colors
|
|
super(colorArray,self).__init__(kwargs)
|
|
|
|
def __getitem__(self, a):
|
|
if isinstance(a, int):
|
|
return super(colorArray, self).__getitem__(
|
|
slice(a*self.n, a*self.n+self.n))
|
|
if isinstance(a, slice):
|
|
start = a.start*self.n if a.start != None else None
|
|
stop = a.stop*self.n if a.stop != None else None
|
|
return colorArray(self.n,
|
|
super(colorArray, self).__getitem__(
|
|
slice(start, stop, a.step)))
|
|
def __getslice__(self, a, b):
|
|
return self.__getitem__(slice(a,b))
|
|
def __setitem__(self, a, v):
|
|
if isinstance(a, int):
|
|
for i in range(self.n):
|
|
super(colorArray, self).__setitem__(a*self.n + i, v[i])
|
|
def __len__(self):
|
|
return super(colorArray, self).__len__() // self.n
|
|
def reverse(self):
|
|
self.__init__(self.n, [c for cl in range(len(self)-1,-1, -1)
|
|
for c in self[cl]])
|
|
def shift(self, shift=1, direction=True):
|
|
if direction:
|
|
shift *= -1
|
|
self.__init__(self.n, self[shift:] + self[:shift])
|
|
def padLeft(self, v, a):
|
|
self.__init__(self.n, v * a + self)
|
|
def padRight(self, v, a):
|
|
self += v * a
|
|
|
|
######################################################################
|
|
# LED Effect handler
|
|
######################################################################
|
|
|
|
class ledFrameHandler:
|
|
def __init__(self, config):
|
|
self.printer = config.get_printer()
|
|
self.gcode = self.printer.lookup_object('gcode')
|
|
self.printer.load_object(config, "display_status")
|
|
self.printer.load_object(config, "print_stats")
|
|
self.heaters = {}
|
|
self.hots = {}
|
|
self.printProgress = 0
|
|
self.effects = []
|
|
self.stepperPositions = [0.0,0.0,0.0]
|
|
self.stepperTimer = None
|
|
self.heaterCurrent = {}
|
|
self.heaterTarget = {}
|
|
self.heaterLast = {}
|
|
self.heaterTimer = None
|
|
self.progressTimer = None
|
|
self.hotTarget = {}
|
|
self.homing = {}
|
|
self.homing_start_flag = {}
|
|
self.homing_end_flag = {}
|
|
self.runStatus = {}
|
|
self.runtStatusMsg = {}
|
|
self.printer.register_event_handler('klippy:ready', self._handle_ready)
|
|
self.printer.register_event_handler("homing:homing_move_begin",
|
|
self._handle_homing_move_begin)
|
|
self.printer.register_event_handler("homing:homing_move_end",
|
|
self._handle_homing_move_end)
|
|
self.ledChains=[]
|
|
self.gcode.register_command('STOP_LED_EFFECTS',
|
|
self.cmd_STOP_LED_EFFECTS,
|
|
desc=self.cmd_STOP_LED_EFFECTS_help)
|
|
self.shutdown = False
|
|
|
|
cmd_STOP_LED_EFFECTS_help = 'Stops all led_effects'
|
|
|
|
def _handle_ready(self):
|
|
self.shutdown = False
|
|
self.reactor = self.printer.get_reactor()
|
|
self.printer.register_event_handler('klippy:shutdown',
|
|
self._handle_shutdown)
|
|
self.printProgress = 0
|
|
self.runStatus = {}
|
|
self.runtStatusMsg = {}
|
|
self.displayStatus = self.printer.lookup_object('display_status')
|
|
self.printStatus = self.printer.lookup_object('print_stats')
|
|
if not self.progressTimer:
|
|
self.progressTimer = self.reactor.register_timer(self._pollProgress,
|
|
self.reactor.NOW)
|
|
self.StatusTimer = self.reactor.register_timer(self._pollStatus,
|
|
self.reactor.NOW)
|
|
self.frameTimer = self.reactor.register_timer(self._getFrames,
|
|
self.reactor.NOW)
|
|
|
|
def _handle_shutdown(self):
|
|
self.shutdown = True
|
|
for effect in self.effects:
|
|
if not effect.runOnShutown:
|
|
for chain in self.ledChains:
|
|
chain.led_helper.set_color(None, (0.0, 0.0, 0.0, 0.0))
|
|
chain.led_helper.update_func(chain.led_helper.led_state, None)
|
|
|
|
pass
|
|
|
|
def _handle_homing_move_begin(self, hmove):
|
|
endstops_being_homed = [name for es,name in hmove.endstops]
|
|
|
|
for endstop in endstops_being_homed:
|
|
if endstop in self.homing_start_flag:
|
|
self.homing_start_flag[endstop] += 1
|
|
else:
|
|
self.homing_start_flag[endstop] = 0
|
|
|
|
self.homing[endstop]=True
|
|
|
|
def _handle_homing_move_end(self, hmove):
|
|
endstops_being_homed = [name for es,name in hmove.endstops]
|
|
|
|
for endstop in endstops_being_homed:
|
|
if endstop in self.homing_end_flag:
|
|
self.homing_end_flag[endstop] += 1
|
|
else:
|
|
self.homing_end_flag[endstop] = 0
|
|
self.homing[endstop]=False
|
|
|
|
def addEffect(self, effect):
|
|
|
|
if effect.heater:
|
|
effect.heater=effect.heater.strip('\"\'')
|
|
if effect.heater.startswith("temperature_fan ") or effect.heater.startswith("temperature_sensor "):
|
|
self.heaters[effect.heater] = self.printer.lookup_object(effect.heater)
|
|
else:
|
|
pheater = self.printer.lookup_object('heaters')
|
|
self.heaters[effect.heater] = pheater.lookup_heater(effect.heater)
|
|
self.heaterLast[effect.heater] = 100
|
|
self.heaterCurrent[effect.heater] = 0
|
|
self.heaterTarget[effect.heater] = 0
|
|
|
|
if not self.heaterTimer:
|
|
self.heaterTimer = self.reactor.register_timer(self._pollHeater,
|
|
self.reactor.NOW)
|
|
if effect.hot:
|
|
effect.hot=effect.hot.strip('\"\'').split(' ')
|
|
pheater = self.printer.lookup_object('heaters')
|
|
for i in range(len(effect.hot)):
|
|
self.hots[effect.hot[i]] = pheater.lookup_heater(effect.hot[i])
|
|
self.heaterTarget[effect.hot[i]] = 0
|
|
|
|
if not self.progressTimer:
|
|
self.progressTimer = self.reactor.register_timer(self._pollStatus,
|
|
self.reactor.NOW)
|
|
|
|
if effect.stepper:
|
|
self.toolhead = self.printer.lookup_object('toolhead')
|
|
self.kin = self.toolhead.get_kinematics()
|
|
|
|
if not self.stepperTimer:
|
|
self.stepperTimer = self.reactor.register_timer(
|
|
self._pollStepper,
|
|
self.reactor.NOW)
|
|
|
|
self.effects.append(effect)
|
|
|
|
def _pollHeater(self, eventtime):
|
|
for heater in self.heaters.keys():
|
|
current, target = self.heaters[heater].get_temp(eventtime)
|
|
self.heaterCurrent[heater] = current
|
|
self.heaterTarget[heater] = target
|
|
if target > 0:
|
|
self.heaterLast[heater] = target
|
|
return eventtime + 0.3 #sensors get updated every 300ms
|
|
|
|
def _pollStepper(self, eventtime):
|
|
|
|
kin_spos = {s.get_name(): s.get_commanded_position()
|
|
for s in self.kin.get_steppers()}
|
|
|
|
pos = self.kin.calc_position(kin_spos)
|
|
|
|
for i in range(3):
|
|
if pos[i] >= self.kin.axes_min[i] and pos[i] <= self.kin.axes_max[i]:
|
|
self.stepperPositions[i] = int(
|
|
((pos[i] - self.kin.axes_min[i]) / \
|
|
(self.kin.axes_max[i] - self.kin.axes_min[i])
|
|
* 100)- 1)
|
|
return eventtime + 0.5
|
|
|
|
def _pollProgress(self, eventtime):
|
|
status = self.displayStatus.get_status(eventtime)
|
|
p = status.get('progress')
|
|
if p is not None:
|
|
self.printProgress = int(p * 100)
|
|
return eventtime + 1
|
|
|
|
def _pollStatus(self, eventtime):
|
|
for heater in self.hots.keys():
|
|
current, target = self.hots[heater].get_temp(eventtime)
|
|
self.hotTarget[heater] = target
|
|
|
|
sta = self.printStatus.get_status(eventtime)
|
|
p = sta.get('state')
|
|
msg = sta.get('message')
|
|
if p is not None:
|
|
self.runStatus = p
|
|
if msg is not None:
|
|
self.runtStatusMsg = msg
|
|
return eventtime + 0.5
|
|
|
|
def _getColorData(self, colors, fade):
|
|
clamp = (lambda x : 0.0 if x < 0.0 else 1.0 if x > 1.0 else x)
|
|
colors = [x*clamp(fade) for x in colors]
|
|
colors=colors + [0.0] * (4 - len(colors))
|
|
colors=colors[:4]
|
|
colors = [clamp(x) for x in colors]
|
|
return tuple(colors)
|
|
|
|
def _getFrames(self, eventtime):
|
|
chainsToUpdate = set()
|
|
|
|
frames = [(effect, effect.getFrame(eventtime)) for effect in self.effects]
|
|
|
|
#first set all LEDs to 0, that should be updated
|
|
for effect, (frame, update) in frames:
|
|
if update:
|
|
for i in range(effect.ledCount):
|
|
chain,index=effect.leds[i]
|
|
chain.led_helper.led_state[index] = (0.0, 0.0, 0.0, 0.0)
|
|
chainsToUpdate.add(chain)
|
|
|
|
#then sum up all effects for that LEDs
|
|
for effect, (frame, update) in frames:
|
|
if update:
|
|
for i in range(effect.ledCount):
|
|
chain,index=effect.leds[i]
|
|
|
|
current_state=list(chain.led_helper.led_state[index])
|
|
effect_state=self._getColorData(frame[i*COLORS:i*COLORS+COLORS],
|
|
effect.fadeValue)
|
|
|
|
next_state=[min(1.0,a+b) for a,b in \
|
|
zip(current_state, effect_state)]
|
|
|
|
chain.led_helper.led_state[index] = tuple(next_state)
|
|
chainsToUpdate.add(chain)
|
|
|
|
for chain in chainsToUpdate:
|
|
if hasattr(chain,"prev_data"):
|
|
chain.prev_data = None # workaround to force update of dotstars
|
|
if not self.shutdown:
|
|
chain.led_helper.update_func(chain.led_helper.led_state, None)
|
|
if self.effects:
|
|
next_eventtime=min(self.effects, key=lambda x: x.nextEventTime)\
|
|
.nextEventTime
|
|
else:
|
|
next_eventtime = eventtime
|
|
# run at least with 10Hz
|
|
next_eventtime=min(next_eventtime, eventtime + 0.1)
|
|
return next_eventtime
|
|
|
|
def parse_chain(self, chain):
|
|
chain = chain.strip()
|
|
leds=[]
|
|
parms = [parameter.strip() for parameter in chain.split()
|
|
if parameter.strip()]
|
|
if parms:
|
|
chainName=parms[0].replace(':',' ')
|
|
ledIndices = ''.join(parms[1:]).strip('()').split(',')
|
|
for led in ledIndices:
|
|
if led:
|
|
if '-' in led:
|
|
start, stop = map(int,led.split('-'))
|
|
if stop == start:
|
|
ledList = [start-1]
|
|
elif stop > start:
|
|
ledList = list(range(start-1, stop))
|
|
else:
|
|
ledList = list(reversed(range(stop-1, start)))
|
|
for i in ledList:
|
|
leds.append(int(i))
|
|
else:
|
|
for i in led.split(','):
|
|
leds.append(int(i)-1)
|
|
|
|
return chainName, leds
|
|
else:
|
|
return None, None
|
|
|
|
def cmd_STOP_LED_EFFECTS(self, gcmd):
|
|
ledParam = gcmd.get('LEDS', "")
|
|
stopAll = (ledParam == "")
|
|
|
|
for effect in self.effects:
|
|
stopEffect = stopAll
|
|
if not stopAll:
|
|
try:
|
|
chainName, ledIndices = self.parse_chain(ledParam)
|
|
chain = self.printer.lookup_object(chainName)
|
|
except Exception as e:
|
|
raise gcmd.error("Unknown LED '%s'" % (ledParam,))
|
|
|
|
if ledIndices == [] and chain in effect.ledChains:
|
|
stopEffect = True
|
|
else:
|
|
for index in ledIndices:
|
|
if (chain,index) in effect.leds:
|
|
stopEffect=True
|
|
|
|
if stopEffect:
|
|
if effect.enabled:
|
|
effect.set_fade_time(gcmd.get_float('FADETIME', 0.0))
|
|
effect.set_enabled(False)
|
|
|
|
def load_config(config):
|
|
return ledFrameHandler(config)
|
|
|
|
######################################################################
|
|
# LED Effect
|
|
######################################################################
|
|
|
|
class ledEffect:
|
|
def __init__(self, config):
|
|
self.config = config
|
|
self.printer = config.get_printer()
|
|
self.gcode = self.printer.lookup_object('gcode')
|
|
self.gcode_macro = self.printer.load_object(config, 'gcode_macro')
|
|
self.handler = self.printer.load_object(config, 'led_effect')
|
|
self.frameRate = 1.0 / config.getfloat('frame_rate',
|
|
default=24, minval=1, maxval=60)
|
|
self.enabled = False
|
|
self.iteration = 0
|
|
self.layers = []
|
|
self.analogValue = 0
|
|
self.button_state = 0
|
|
self.fadeValue = 0.0
|
|
self.fadeTime = 0.0
|
|
self.fadeEndTime = 0
|
|
|
|
#Basic functions for layering colors. t=top and b=bottom color
|
|
self.blendingModes = {
|
|
'top' : (lambda t, b: t ),
|
|
'bottom' : (lambda t, b: b ),
|
|
'add' : (lambda t, b: t + b ),
|
|
'subtract' : (lambda t, b: (b - t) * (b - t > 0)),
|
|
'subtract_b': (lambda t, b: (t - b) * (t - b > 0)),
|
|
'difference': (lambda t, b: (t - b) * (t > b) + (b - t) * (t <= b)),
|
|
'average' : (lambda t, b: 0.5 * (t + b)),
|
|
'multiply' : (lambda t, b: t * b),
|
|
'divide' : (lambda t, b: t / b if b > 0 else 0 ),
|
|
'divide_inv': (lambda t, b: b / t if t > 0 else 0 ),
|
|
'screen' : (lambda t, b: 1.0 - (1.0-t)*(1.0-b) ),
|
|
'lighten' : (lambda t, b: t * (t > b) + b * (t <= b)),
|
|
'darken' : (lambda t, b: t * (t < b) + b * (t >= b)),
|
|
'overlay' : (lambda t, b: \
|
|
2.0 * t * b if t > 0.5 else \
|
|
1.0 - (2.0 * (1.0-t) * (1.0-b)))
|
|
}
|
|
|
|
self.name = config.get_name().split()[1]
|
|
|
|
self.autoStart = config.getboolean('autostart', False)
|
|
self.runOnShutown = config.getboolean('run_on_error', False)
|
|
self.heater = config.get('heater', None)
|
|
self.hot = config.get('hot', None)
|
|
self.analogPin = config.get('analog_pin', None)
|
|
self.buttonPins = config.getlist('button_pins', None)
|
|
self.stepper = config.get('stepper', None)
|
|
self.recalculate = config.get('recalculate', False)
|
|
self.endstops = [x.strip() for x in config.get('endstops','').split(',')]
|
|
self.layerTempl = self.gcode_macro.load_template(config, 'layers')
|
|
self.configLayers = []
|
|
self.configLeds = config.get('leds')
|
|
|
|
self.nextEventTime = 0
|
|
self.printer.register_event_handler('klippy:ready', self._handle_ready)
|
|
self.gcode.register_mux_command('SET_LED_EFFECT', 'EFFECT', self.name,
|
|
self.cmd_SET_LED_EFFECT,
|
|
desc=self.cmd_SET_LED_help)
|
|
|
|
if self.analogPin:
|
|
ppins = self.printer.lookup_object('pins')
|
|
self.mcu_adc = ppins.setup_pin('adc', self.analogPin)
|
|
self.mcu_adc.setup_adc_sample(ANALOG_SAMPLE_TIME, ANALOG_SAMPLE_COUNT)
|
|
self.mcu_adc.setup_adc_callback(ANALOG_REPORT_TIME, self.adcCallback)
|
|
query_adc = self.printer.load_object(self.config, 'query_adc')
|
|
query_adc.register_adc(self.name, self.mcu_adc)
|
|
|
|
if self.buttonPins:
|
|
buttons = self.printer.load_object(config, "buttons")
|
|
buttons.register_buttons(self.buttonPins, self.button_callback)
|
|
|
|
cmd_SET_LED_help = 'Starts or Stops the specified led_effect'
|
|
|
|
def _handle_ready(self):
|
|
self.configChains = self.configLeds.split('\n')
|
|
self.ledChains = []
|
|
self.leds = []
|
|
self.enabled = self.autoStart
|
|
if not self.enabled:
|
|
self.nextEventTime = self.handler.reactor.NEVER
|
|
self.printer.register_event_handler('klippy:shutdown',
|
|
self._handle_shutdown)
|
|
#map each LED from the chains to the "pixels" in the effect frame
|
|
for chain in self.configChains:
|
|
chainName, ledIndices = self.handler.parse_chain(chain)
|
|
if chainName is not None:
|
|
ledChain = self.printer.lookup_object(chainName)
|
|
|
|
#Add each discrete chain to the collection
|
|
if ledChain not in self.ledChains:
|
|
self.ledChains.append(ledChain)
|
|
|
|
if ledIndices == [] :
|
|
for i in range(ledChain.led_helper.led_count):
|
|
self.leds.append((ledChain, int(i)))
|
|
else:
|
|
for led in ledIndices:
|
|
self.leds.append((ledChain, led))
|
|
|
|
self.ledCount = len(self.leds)
|
|
self.frame = [0.0] * COLORS * self.ledCount
|
|
|
|
#enumerate all effects from the subclasses of _layerBase...
|
|
self.availableLayers = {str(c).rpartition('.layer')[2]\
|
|
.replace("'>", "")\
|
|
.lower() : c
|
|
for c in self._layerBase.__subclasses__()
|
|
if str(c).startswith("<class")}
|
|
self._generateLayers()
|
|
|
|
def _generateLayers(self, context=None):
|
|
self.layers = []
|
|
if context is None:
|
|
context = self.gcode_macro.create_template_context()
|
|
context.update({'params': {}, 'rawparams': ''})
|
|
self.configLayers = self.layerTempl.render(context)
|
|
for layer in [line for line \
|
|
in self.configLayers.split('\n') if line.strip()]:
|
|
|
|
parms = [parameter.strip() for parameter \
|
|
in layer.split() if parameter.strip()]
|
|
|
|
if not parms[0] in self.availableLayers:
|
|
raise self.printer\
|
|
.config_error("LED Effect '%s' in section '%s' is not a " \
|
|
"valid effect layer" % (parms[0], self.name))
|
|
|
|
if not parms[3] in self.blendingModes:
|
|
raise self.printer.config_error("Blending mode '%s' in section "
|
|
"'%s' is not a valid blending mode"\
|
|
% (parms[3], self.name))
|
|
|
|
layer = self.availableLayers[parms[0]]
|
|
|
|
pad = lambda x: x + [0.0] * (COLORS - len(x))
|
|
convert = lambda s: float(s)
|
|
|
|
try:
|
|
palette="".join(parms[4:]) # join all elements of the list
|
|
palette="".join(palette.split()) # remove whitespaces
|
|
palette=palette.strip(",")
|
|
palette=palette.split("),(") # split colors
|
|
palette=[c.split(",") for c in palette] # split color components
|
|
palette=[[convert(k.strip("()")) for k in c] for c in palette] # convert to float
|
|
for i in palette:
|
|
if len(i) > COLORS:
|
|
raise Exception(
|
|
"Color %s has too many elements." % (str(i),))
|
|
palette=[pad(c) for c in palette] # pad to COLORS colors
|
|
palette=[k for c in palette for k in c] # flatten list
|
|
except Exception as e:
|
|
raise self.printer.config_error(
|
|
"Error parsing palette in '%s' for layer \"%s\": %s"\
|
|
% (self.config.get_name(), parms[0], e,))
|
|
self.layers.insert(0, layer(handler = self,
|
|
frameHandler = self.handler,
|
|
effectRate = float(parms[1]),
|
|
effectCutoff = float(parms[2]),
|
|
paletteColors = palette,
|
|
frameRate = self.frameRate,
|
|
ledCount = len(self.leds),
|
|
blendingMode = parms[3]))
|
|
|
|
self.handler.addEffect(self)
|
|
|
|
def getFrame(self, eventtime):
|
|
if not self.enabled and self.fadeValue <= 0.0:
|
|
if self.nextEventTime < self.handler.reactor.NEVER:
|
|
# Effect has just been disabled. Set colors to 0 and update once.
|
|
self.nextEventTime = self.handler.reactor.NEVER
|
|
self.frame = [0.0] * COLORS * self.ledCount
|
|
update = True
|
|
else:
|
|
update = False
|
|
else:
|
|
update = True
|
|
if eventtime >= self.nextEventTime:
|
|
self.nextEventTime = eventtime + self.frameRate
|
|
|
|
self.frame = [0.0] * COLORS * self.ledCount
|
|
for layer in self.layers:
|
|
layerFrame = layer.nextFrame(eventtime)
|
|
|
|
if layerFrame:
|
|
blend = self.blendingModes[layer.blendingMode]
|
|
self.frame = [blend(t, b) for t, b in zip(layerFrame, self.frame)]
|
|
|
|
if (self.fadeEndTime > eventtime) and (self.fadeTime > 0.0):
|
|
remainingFade = ((self.fadeEndTime - eventtime) / self.fadeTime)
|
|
else:
|
|
remainingFade = 0.0
|
|
|
|
self.fadeValue = 1.0-remainingFade if self.enabled else remainingFade
|
|
|
|
return self.frame, update
|
|
|
|
def set_enabled(self, state):
|
|
if self.enabled != state:
|
|
self.enabled = state
|
|
self.nextEventTime = self.handler.reactor.NOW
|
|
self.handler._getFrames(self.handler.reactor.NOW)
|
|
|
|
def reset_frame(self):
|
|
for layer in self.layers:
|
|
layer.frameNumber = 0
|
|
|
|
def set_fade_time(self, fadetime):
|
|
self.fadeTime = fadetime
|
|
self.fadeEndTime = self.handler.reactor.monotonic() + fadetime
|
|
if self.fadeTime == 0.0:
|
|
self.fadeValue = 0.0
|
|
|
|
def cmd_SET_LED_EFFECT(self, gcmd):
|
|
parmFadeTime = gcmd.get_float('FADETIME', 0.0)
|
|
|
|
if gcmd.get_int('STOP', 0) >= 1:
|
|
if self.enabled:
|
|
self.set_fade_time(parmFadeTime)
|
|
self.set_enabled(False)
|
|
else:
|
|
if self.recalculate:
|
|
kwargs = self.layerTempl.create_template_context()
|
|
kwargs['params'] = gcmd.get_command_parameters()
|
|
kwargs['rawparams'] = gcmd.get_raw_command_parameters()
|
|
self._generateLayers(kwargs)
|
|
if gcmd.get_int('REPLACE',0) >= 1:
|
|
for led in self.leds:
|
|
for effect in self.handler.effects:
|
|
if effect is not self and led in effect.leds:
|
|
if effect.enabled:
|
|
effect.set_fade_time(parmFadeTime)
|
|
effect.set_enabled(False)
|
|
|
|
if not self.enabled:
|
|
self.set_fade_time(parmFadeTime)
|
|
if gcmd.get_int('RESTART', 0) >= 1:
|
|
self.reset_frame()
|
|
self.set_enabled(True)
|
|
|
|
def _handle_shutdown(self):
|
|
self.set_enabled(self.runOnShutown)
|
|
|
|
def adcCallback(self, read_time, read_value):
|
|
self.analogValue = int(read_value * 1000.0) / 10.0
|
|
|
|
def button_callback(self, eventtime, state):
|
|
self.button_state = state
|
|
|
|
######################################################################
|
|
# LED Effect layers
|
|
######################################################################
|
|
|
|
# super class for effect animations. new animations should
|
|
# inherit this and return 1 frame of [r, g, b] * <number of leds>
|
|
# per call of nextFrame()
|
|
class _layerBase(object):
|
|
def __init__(self, **kwargs):
|
|
self.handler = kwargs['handler']
|
|
self.frameHandler = kwargs['frameHandler']
|
|
self.ledCount = kwargs['ledCount']
|
|
self.paletteColors = colorArray(COLORS, kwargs['paletteColors'])
|
|
self.effectRate = kwargs['effectRate']
|
|
self.effectCutoff = kwargs['effectCutoff']
|
|
self.frameRate = kwargs['frameRate']
|
|
self.blendingMode = kwargs['blendingMode']
|
|
self.frameNumber = 0
|
|
self.thisFrame = []
|
|
self.frameCount = 1
|
|
self.lastAnalog = 0
|
|
|
|
def nextFrame(self, eventtime):
|
|
if not self.frameCount:
|
|
return [0] * COLORS * self.ledCount
|
|
self.frameNumber += 1
|
|
self.frameNumber = self.frameNumber * \
|
|
( self.frameNumber < self.frameCount )
|
|
self.lastFrameTime = eventtime
|
|
|
|
return self.thisFrame[self.frameNumber]
|
|
|
|
def _decayTable(self, factor=1, rate=1):
|
|
|
|
frame = []
|
|
|
|
p = (1.0 / self.frameRate)
|
|
r = (p/15.0)*factor
|
|
|
|
for s in range(0, int((rate<1)+rate)):
|
|
frame.append(1.0)
|
|
for x in range(2, int(p / rate)):
|
|
b = exp(1)**-(x/r)
|
|
if b>.004:
|
|
frame.append(b)
|
|
return frame
|
|
|
|
def _gradient(self, palette, steps, reverse=False, toFirst=False):
|
|
palette = colorArray(COLORS, palette[:])
|
|
if reverse: palette.reverse()
|
|
|
|
if len(palette) == 1:
|
|
return colorArray(COLORS, palette * steps)
|
|
|
|
if toFirst:
|
|
palette += palette[0]
|
|
|
|
paletteIntervals = len(palette)-1
|
|
stepIntervals = steps if toFirst else steps-1
|
|
if stepIntervals != 0:
|
|
intervals_per_step = float(paletteIntervals) / stepIntervals
|
|
else:
|
|
intervals_per_step = 0
|
|
|
|
gradient=palette[0]
|
|
|
|
for i in range(1,steps):
|
|
j = intervals_per_step * i
|
|
k = int(j)
|
|
r = j-k
|
|
k = min(k, len(palette)-1)
|
|
|
|
if ( (k+1) >= len(palette) ) | (r == 0.0) :
|
|
z = palette[k]
|
|
else:
|
|
z = [((1-r)*palette[k][m] + r*palette[k+1][m]) for m in range(COLORS)]
|
|
gradient += z
|
|
return gradient
|
|
|
|
#Individual effects inherit from the LED Effect Base class
|
|
#each effect must support the nextFrame() method either by
|
|
#using the method from the base class or overriding it.
|
|
|
|
#Solid color
|
|
class layerStatic(_layerBase):
|
|
def __init__(self, **kwargs):
|
|
super(ledEffect.layerStatic, self).__init__(**kwargs)
|
|
|
|
self.paletteColors = colorArray(COLORS, self.paletteColors)
|
|
|
|
gradientLength = int(self.ledCount)
|
|
gradient = colorArray(COLORS, self._gradient(self.paletteColors,
|
|
gradientLength))
|
|
|
|
self.thisFrame.append(gradient[0:self.ledCount])
|
|
self.frameCount = len(self.thisFrame)
|
|
|
|
#Slow pulsing of color
|
|
class layerBreathing(_layerBase):
|
|
def __init__(self, **kwargs):
|
|
super(ledEffect.layerBreathing, self).__init__(**kwargs)
|
|
|
|
brightness = []
|
|
|
|
p = (1 / self.frameRate) * (self.effectRate * 0.5)
|
|
o = int(p)
|
|
f = 2 * pi
|
|
|
|
for x in range(0, int(p)):
|
|
if x < p:
|
|
v = (exp(-cos((f / p) * (x+o)))-0.367879) / 2.35040238
|
|
else:
|
|
v = 0
|
|
|
|
#clamp values
|
|
if v > 1.0:
|
|
v = 1.0
|
|
elif v < 0.0:
|
|
v = 0.0
|
|
|
|
brightness.append(v)
|
|
|
|
for c in range(0, len(self.paletteColors)):
|
|
color = self.paletteColors[c]
|
|
|
|
for b in brightness:
|
|
self.thisFrame += [[b * i for i in color] * self.ledCount]
|
|
|
|
self.frameCount = len(self.thisFrame)
|
|
class layerLinearFade(_layerBase):
|
|
def __init__(self, **kwargs):
|
|
super(ledEffect.layerLinearFade, self).__init__(**kwargs)
|
|
|
|
gradientLength = int(self.effectRate / self.frameRate)
|
|
if gradientLength == 0: gradientLength = 1
|
|
|
|
gradient = colorArray(COLORS, self._gradient(self.paletteColors,
|
|
gradientLength, toFirst=True))
|
|
|
|
for i in range(gradientLength):
|
|
self.thisFrame.append(gradient[i]*self.ledCount)
|
|
|
|
self.frameCount = len(self.thisFrame)
|
|
|
|
#Turns the entire strip on and off
|
|
class layerBlink(_layerBase):
|
|
def __init__(self, **kwargs):
|
|
super(ledEffect.layerBlink, self).__init__(**kwargs)
|
|
|
|
dutyCycle= max(0,min(1.0, self.effectCutoff))
|
|
frameCountOn = int(( 1.0 / self.frameRate ) * self.effectRate\
|
|
* dutyCycle)
|
|
frameCountOff = int(( 1.0 / self.frameRate ) * self.effectRate\
|
|
* (1-dutyCycle))
|
|
|
|
for c in range(0, len(self.paletteColors)):
|
|
color = self.paletteColors[c]
|
|
self.thisFrame += [color * self.ledCount] * frameCountOn
|
|
self.thisFrame += [[0]*COLORS * self.ledCount] * frameCountOff
|
|
|
|
self.frameCount = len(self.thisFrame)
|
|
|
|
#Random flashes with decay
|
|
class layerTwinkle(_layerBase):
|
|
def __init__(self, **kwargs):
|
|
super(ledEffect.layerTwinkle, self).__init__(**kwargs)
|
|
|
|
self.thisFrame = colorArray(COLORS, ([0.0]*COLORS) * self.ledCount)
|
|
self.lastBrightness = [-1] * self.ledCount
|
|
self.decayTable = self._decayTable(factor=1 / self.effectCutoff)
|
|
self.decayLen = len(self.decayTable)
|
|
self.colorCount = len(self.paletteColors) - 1
|
|
|
|
def nextFrame(self, eventtime):
|
|
|
|
for i in range(0, self.ledCount):
|
|
|
|
r = randint(0, self.colorCount)
|
|
color = self.paletteColors[r]
|
|
|
|
if randint(0, 255) > 254 - self.effectRate:
|
|
self.lastBrightness[i] = 0
|
|
self.thisFrame[i] = color
|
|
|
|
if self.lastBrightness[i] != -1:
|
|
if self.lastBrightness[i] == self.decayLen:
|
|
self.lastBrightness[i] = -1
|
|
self.thisFrame[i] = ([0.0]*COLORS)
|
|
else:
|
|
x = self.lastBrightness[i]
|
|
self.lastBrightness[i] += 1
|
|
self.thisFrame[i] = [self.decayTable[x] * l
|
|
for l in self.thisFrame[i]]
|
|
|
|
return self.thisFrame
|
|
|
|
#Blinking with decay
|
|
class layerStrobe(_layerBase):
|
|
def __init__(self, **kwargs):
|
|
super(ledEffect.layerStrobe, self).__init__(**kwargs)
|
|
|
|
frameRate = int(1.0 / self.frameRate)
|
|
if self.effectRate==0:
|
|
frameCount = 1
|
|
else:
|
|
frameCount = max(1,int(frameRate / self.effectRate))
|
|
if self.effectCutoff==0: self.effectCutoff=0.001
|
|
decayTable = self._decayTable(factor=1 / self.effectCutoff,
|
|
rate=1)
|
|
if len(decayTable) > frameCount:
|
|
decayTable = decayTable[:frameCount]
|
|
else:
|
|
decayTable += [0.0] * (frameCount - len(decayTable))
|
|
|
|
for c in range(0, len(self.paletteColors)):
|
|
color = self.paletteColors[c]
|
|
|
|
for b in decayTable:
|
|
self.thisFrame += [[b * i for i in color] * self.ledCount]
|
|
|
|
self.frameCount = len(self.thisFrame)
|
|
|
|
#Lights move sequentially with decay
|
|
class layerComet(_layerBase):
|
|
def __init__(self, **kwargs):
|
|
super(ledEffect.layerComet, self).__init__(**kwargs)
|
|
if self.effectRate > 0:
|
|
self.direction = True
|
|
else:
|
|
self.direction = False
|
|
self.effectRate *= -1
|
|
|
|
if self.effectCutoff <= 0: self.effectCutoff = .1
|
|
|
|
decayTable = self._decayTable(factor=len(self.paletteColors) * \
|
|
self.effectCutoff, rate=1)
|
|
|
|
gradient = self.paletteColors[0] + \
|
|
self._gradient(self.paletteColors[1:], len(decayTable)+1)
|
|
|
|
decayTable = [c for b in zip(decayTable, decayTable, decayTable, decayTable) \
|
|
for c in b]
|
|
|
|
comet = colorArray(COLORS, [a * b for a, b in zip(gradient,decayTable)])
|
|
|
|
comet.padRight([0.0]*COLORS, self.ledCount - len(comet))
|
|
|
|
if self.direction: comet.reverse()
|
|
else: comet.shift(self.ledCount - len(comet))
|
|
|
|
if self.effectRate == 0:
|
|
self.thisFrame.append(comet[0:self.ledCount])
|
|
else:
|
|
for i in range(len(comet)):
|
|
comet.shift(int(self.effectRate+(self.effectRate < 1)),
|
|
self.direction)
|
|
self.thisFrame.append(comet[:self.ledCount])
|
|
|
|
for x in range(int((1/self.effectRate)-(self.effectRate <= 1))):
|
|
self.thisFrame.append(comet[:self.ledCount])
|
|
|
|
self.frameCount = len(self.thisFrame)
|
|
|
|
#Lights move sequentially with decay
|
|
class layerChase(_layerBase):
|
|
def __init__(self, **kwargs):
|
|
super(ledEffect.layerChase, self).__init__(**kwargs)
|
|
|
|
if self.effectRate > 0:
|
|
self.direction = True
|
|
else:
|
|
self.direction = False
|
|
self.effectRate *= -1
|
|
|
|
if len(self.paletteColors) == 1:
|
|
self.paletteColors += colorArray(COLORS,COLORS*[0])
|
|
|
|
decayTable = self._decayTable(factor=len(self.paletteColors) * \
|
|
self.effectCutoff, rate=1)
|
|
|
|
gradient = self.paletteColors[0] + \
|
|
self._gradient(self.paletteColors[1:], len(decayTable)+1)
|
|
|
|
decayTable = [c for b in zip(decayTable, decayTable, decayTable, decayTable) \
|
|
for c in b]
|
|
gradient = colorArray(COLORS, [a * b
|
|
for a, b in zip(gradient,decayTable)])
|
|
|
|
k=int(self.ledCount/len(gradient))+1
|
|
chase = colorArray(COLORS,k*gradient)
|
|
|
|
if self.direction: chase.reverse()
|
|
if self.effectRate == 0:
|
|
self.thisFrame.append(chase[0:self.ledCount])
|
|
else:
|
|
for _ in range(len(chase)):
|
|
chase.shift(int(self.effectRate+(self.effectRate < 1)),
|
|
self.direction)
|
|
self.thisFrame.append(chase[0:self.ledCount])
|
|
|
|
for _ in range(int((1/self.effectRate)-(self.effectRate <= 1))):
|
|
self.thisFrame.append(chase[0:self.ledCount])
|
|
|
|
self.frameCount = len(self.thisFrame)
|
|
|
|
#Color gradient over all LEDs
|
|
class layerGradient(_layerBase):
|
|
def __init__(self, **kwargs):
|
|
super(ledEffect.layerGradient, self).__init__(**kwargs)
|
|
|
|
direction = -1 if self.effectRate < 0 else 1
|
|
|
|
if self.effectRate == 0:
|
|
gradientLength = self.ledCount
|
|
else:
|
|
gradientLength=abs(int(1/(self.effectRate * self.frameRate)))
|
|
gradient = colorArray(COLORS, self._gradient(self.paletteColors,
|
|
gradientLength,
|
|
toFirst=True))
|
|
|
|
for i in range(gradientLength if self.effectRate != 0 else 1):
|
|
frame = colorArray(COLORS, ([0.0]*COLORS) * self.ledCount)
|
|
for led in range(self.ledCount):
|
|
frame[led] = gradient[ int(i*direction + \
|
|
self.effectCutoff * gradientLength * led \
|
|
/ self.ledCount ) % gradientLength]
|
|
self.thisFrame.append(frame)
|
|
|
|
self.frameCount = len(self.thisFrame)
|
|
|
|
class layerPattern(_layerBase):
|
|
def __init__(self, **kwargs):
|
|
super(ledEffect.layerPattern, self).__init__(**kwargs)
|
|
|
|
self.paletteColors = colorArray(COLORS, self.paletteColors)
|
|
frame = colorArray(COLORS, [])
|
|
|
|
for i in range(int(self.ledCount/len(self.paletteColors))+1):
|
|
frame+=(self.paletteColors)
|
|
|
|
if int(self.effectRate/self.frameRate) == 0:
|
|
self.thisFrame.append(frame)
|
|
else:
|
|
for _ in range(len(self.paletteColors) * (self.ledCount-1)):
|
|
for _ in range(int(self.effectRate/self.frameRate)):
|
|
self.thisFrame.append(colorArray(COLORS, frame)[:COLORS*self.ledCount])
|
|
frame.shift(int(self.effectCutoff))
|
|
|
|
self.frameCount = len(self.thisFrame)
|
|
|
|
#Responds to heater temperature
|
|
class layerHeater(_layerBase):
|
|
def __init__(self, **kwargs):
|
|
super(ledEffect.layerHeater, self).__init__(**kwargs)
|
|
|
|
if len(self.paletteColors) == 1:
|
|
self.paletteColors += self.paletteColors
|
|
|
|
gradient = colorArray(COLORS, self._gradient(self.paletteColors[:-1], 200) +
|
|
self.paletteColors[-1:])
|
|
|
|
for i in range(len(gradient)):
|
|
self.thisFrame.append(gradient[i] * self.ledCount)
|
|
|
|
self.frameCount = len(self.thisFrame)
|
|
|
|
if self.handler.heater is None:
|
|
raise self.handler.printer.config_error(
|
|
"LED Effect '%s' has no heater defined." % (self.handler.name))
|
|
|
|
def nextFrame(self, eventtime):
|
|
heaterTarget = self.frameHandler.heaterTarget[self.handler.heater]
|
|
heaterCurrent = self.frameHandler.heaterCurrent[self.handler.heater]
|
|
heaterLast = self.frameHandler.heaterLast[self.handler.heater]
|
|
|
|
if heaterTarget > 0.0 and heaterCurrent > 0.0:
|
|
if (heaterCurrent >= self.effectRate):
|
|
if (heaterCurrent <= heaterTarget-2):
|
|
s = int(((heaterCurrent - self.effectRate) / heaterTarget) * 200)
|
|
s = min(len(self.thisFrame)-1,s)
|
|
return self.thisFrame[s]
|
|
elif self.effectCutoff > 0:
|
|
return None
|
|
else:
|
|
return self.thisFrame[-1]
|
|
else:
|
|
return None
|
|
|
|
elif self.effectRate > 0 and heaterCurrent > 0.0:
|
|
if heaterCurrent >= self.effectRate and heaterLast > 0:
|
|
s = int(((heaterCurrent - self.effectRate) / heaterLast) * 200)
|
|
s = min(len(self.thisFrame)-1,s)
|
|
return self.thisFrame[s]
|
|
|
|
return None
|
|
|
|
#Responds to heater temperature
|
|
class layerTemperature(_layerBase):
|
|
def __init__(self, **kwargs):
|
|
|
|
super(ledEffect.layerTemperature, self).__init__(**kwargs)
|
|
if len(self.paletteColors) == 1:
|
|
self.paletteColors = colorArray(COLORS, ([0.0]*COLORS)) + self.paletteColors
|
|
gradient = colorArray(COLORS, self._gradient(self.paletteColors, 200))
|
|
for i in range(len(gradient)):
|
|
self.thisFrame.append(gradient[i] * self.ledCount)
|
|
self.frameCount = len(self.thisFrame)
|
|
|
|
if self.handler.heater is None:
|
|
raise self.handler.printer.config_error(
|
|
"LED Effect '%s' has no heater defined." % (self.handler.name))
|
|
|
|
def nextFrame(self, eventtime):
|
|
if self.effectCutoff == self.effectRate:
|
|
s = 200 if self.frameHandler.heaterCurrent[self.handler.heater] >= self.effectRate else 0
|
|
else:
|
|
s = int(((self.frameHandler.heaterCurrent[self.handler.heater] -
|
|
self.effectRate) /
|
|
(self.effectCutoff - self.effectRate)) * 200)
|
|
|
|
s = min(len(self.thisFrame)-1,s)
|
|
s = max(0,s)
|
|
return self.thisFrame[s]
|
|
class layerHeaterGauge(_layerBase):
|
|
def __init__(self, **kwargs):
|
|
super(ledEffect.layerHeaterGauge, self).__init__(**kwargs)
|
|
|
|
if self.effectRate < 0:
|
|
self.effectRate = self.ledCount
|
|
|
|
if self.effectCutoff < 0:
|
|
self.effectCutoff = self.ledCount
|
|
|
|
if self.effectRate == 0:
|
|
trailing = colorArray(COLORS, [0.0]*COLORS * self.ledCount)
|
|
else:
|
|
trailing = colorArray(COLORS, self._gradient(self.paletteColors[1:],
|
|
int(self.effectRate), True))
|
|
trailing.padLeft([0.0]*COLORS, self.ledCount)
|
|
|
|
if self.effectCutoff == 0:
|
|
leading = colorArray(COLORS, [0.0]*COLORS * self.ledCount)
|
|
else:
|
|
leading = colorArray(COLORS, self._gradient(self.paletteColors[1:],
|
|
int(self.effectCutoff), False))
|
|
leading.padRight([0.0]*COLORS, self.ledCount)
|
|
|
|
gradient = colorArray(COLORS, trailing + self.paletteColors[0] + leading)
|
|
gradient.shift(len(trailing), 0)
|
|
frames = [gradient[:self.ledCount]]
|
|
|
|
for i in range(0, self.ledCount):
|
|
gradient.shift(1,1)
|
|
frames.append(gradient[:self.ledCount])
|
|
|
|
self.thisFrame.append(colorArray(COLORS, [0.0]*COLORS * self.ledCount))
|
|
for i in range(1, 101):
|
|
x = int((i / 101.0) * self.ledCount)
|
|
self.thisFrame.append(frames[x])
|
|
|
|
self.frameCount = len(self.thisFrame)
|
|
|
|
def nextFrame(self, eventtime):
|
|
heaterTarget = self.frameHandler.heaterTarget[self.handler.heater]
|
|
heaterCurrent = self.frameHandler.heaterCurrent[self.handler.heater]
|
|
heaterLast = self.frameHandler.heaterLast[self.handler.heater]
|
|
|
|
if heaterTarget > 0.0:
|
|
p = int(heaterCurrent/heaterTarget * 100.0)
|
|
elif heaterLast > 0.0:
|
|
p = int(heaterCurrent/heaterLast * 100.0)
|
|
else:
|
|
p = 0
|
|
|
|
p = min(len(self.thisFrame)-1,p)
|
|
p = max(0,p)
|
|
|
|
return self.thisFrame[p]
|
|
|
|
class layerTemperatureGauge(_layerBase):
|
|
def __init__(self, **kwargs):
|
|
super(ledEffect.layerTemperatureGauge, self).__init__(**kwargs)
|
|
|
|
trailing = colorArray(COLORS, self._gradient(self.paletteColors[1:],
|
|
int(self.ledCount), True))
|
|
trailing.padLeft([0.0]*COLORS, self.ledCount)
|
|
|
|
leading = colorArray(COLORS, [0.0]*COLORS * self.ledCount)
|
|
|
|
gradient = colorArray(COLORS, trailing + self.paletteColors[0] + leading)
|
|
gradient.shift(len(trailing), 0)
|
|
frames = [gradient[:self.ledCount]]
|
|
|
|
for i in range(0, self.ledCount):
|
|
gradient.shift(1,1)
|
|
frames.append(gradient[:self.ledCount])
|
|
|
|
self.thisFrame.append(colorArray(COLORS, [0.0]*COLORS * self.ledCount))
|
|
self.steps = 255
|
|
for i in range(1, self.steps + 1):
|
|
x = int((i / float(self.steps + 1)) * self.ledCount)
|
|
frames2=colorArray(COLORS,[])
|
|
|
|
for idx,led in enumerate(frames[x]):
|
|
|
|
brightness = min(1.0,max(0.0,len(frames[x]) * (float(i) / float(self.steps + 1)) - int(idx/COLORS)))
|
|
|
|
frames2.append(led*brightness)
|
|
|
|
self.thisFrame.append(frames2)
|
|
|
|
self.frameCount = len(self.thisFrame)
|
|
|
|
def nextFrame(self, eventtime):
|
|
if self.effectCutoff == self.effectRate:
|
|
s = len(self.thisFrame) if self.frameHandler.heaterCurrent[self.handler.heater] >= self.effectRate else 0
|
|
else:
|
|
s = int(((self.frameHandler.heaterCurrent[self.handler.heater] -
|
|
self.effectRate) /
|
|
(self.effectCutoff - self.effectRate)) * self.steps)
|
|
|
|
s = min(len(self.thisFrame)-1,s)
|
|
s = max(0,s)
|
|
|
|
|
|
|
|
return self.thisFrame[s]
|
|
|
|
|
|
#Responds to analog pin voltage
|
|
class layerAnalogPin(_layerBase):
|
|
def __init__(self, **kwargs):
|
|
super(ledEffect.layerAnalogPin, self).__init__(**kwargs)
|
|
|
|
if len(self.paletteColors) == 1:
|
|
self.paletteColors = [0.0]*COLORS + self.paletteColors
|
|
|
|
gradient = colorArray(COLORS, self._gradient(self.paletteColors, 101))
|
|
|
|
for i in range(len(gradient)):
|
|
self.thisFrame.append(gradient[i] * self.ledCount)
|
|
|
|
def nextFrame(self, eventtime):
|
|
v = int(self.handler.analogValue * self.effectRate)
|
|
|
|
if v > 100: v = 100
|
|
|
|
if v > self.effectCutoff:
|
|
return self.thisFrame[v]
|
|
else:
|
|
return self.thisFrame[0]
|
|
|
|
#Lights illuminate relative to stepper position
|
|
class layerStepper(_layerBase):
|
|
def __init__(self, **kwargs):
|
|
super(ledEffect.layerStepper, self).__init__(**kwargs)
|
|
|
|
if self.effectRate < 0:
|
|
self.effectRate = self.ledCount
|
|
|
|
if self.effectCutoff < 0:
|
|
self.effectCutoff = self.ledCount
|
|
|
|
if self.effectRate == 0:
|
|
trailing = colorArray(COLORS, [0.0]*COLORS * self.ledCount)
|
|
else:
|
|
trailing = colorArray(COLORS, self._gradient(self.paletteColors[1:],
|
|
int(self.effectRate), True))
|
|
trailing.padLeft([0.0]*COLORS, self.ledCount)
|
|
|
|
if self.effectCutoff == 0:
|
|
leading = colorArray(COLORS, [0.0]*COLORS * self.ledCount)
|
|
else:
|
|
leading = colorArray(COLORS, self._gradient(self.paletteColors[1:],
|
|
int(self.effectCutoff), False))
|
|
leading.padRight([0.0]*COLORS, self.ledCount)
|
|
|
|
gradient = colorArray(COLORS, trailing + self.paletteColors[0] + leading)
|
|
gradient.shift(len(trailing)-1, 0)
|
|
frames = [gradient[:self.ledCount]]
|
|
|
|
for i in range(0, self.ledCount):
|
|
gradient.shift(1,1)
|
|
frames.append(gradient[:self.ledCount])
|
|
|
|
for i in range(101):
|
|
x = int((i / 101.0) * self.ledCount)
|
|
self.thisFrame.append(frames[x])
|
|
|
|
self.frameCount = len(self.thisFrame)
|
|
|
|
def nextFrame(self, eventtime):
|
|
if self.handler.stepper == 'x': axis = 0
|
|
elif self.handler.stepper == 'y': axis = 1
|
|
else: axis = 2
|
|
|
|
p = self.frameHandler.stepperPositions[int(axis)]
|
|
|
|
if p < 0 : p=0
|
|
if p > 100 : p=100
|
|
return self.thisFrame[int((p - 1) * (p > 0))]
|
|
|
|
class layerStepperColor(_layerBase):
|
|
def __init__(self, **kwargs):
|
|
super(ledEffect.layerStepperColor, self).__init__(**kwargs)
|
|
|
|
if len(self.paletteColors) == 1:
|
|
self.paletteColors = [0.0]*COLORS + self.paletteColors
|
|
|
|
gradient = colorArray(COLORS, self._gradient(self.paletteColors, 101))
|
|
|
|
for i in range(len(gradient)):
|
|
self.thisFrame.append(gradient[i] * self.ledCount)
|
|
|
|
def nextFrame(self, eventtime):
|
|
if self.handler.stepper == 'x': axis = 0
|
|
elif self.handler.stepper == 'y': axis = 1
|
|
else: axis = 2
|
|
|
|
p = self.frameHandler.stepperPositions[int(axis)]*self.effectRate+self.effectCutoff
|
|
|
|
if p < 0 : p=0
|
|
if p > 100 : p=100
|
|
|
|
return self.thisFrame[int(p)]
|
|
|
|
#Shameless port of Fire2012 by Mark Kriegsman
|
|
|
|
#Shamelessly appropriated from the Arduino FastLED example files
|
|
#Fire2012.ino by Daniel Garcia
|
|
class layerFire(_layerBase):
|
|
def __init__(self, **kwargs):
|
|
super(ledEffect.layerFire, self).__init__(**kwargs)
|
|
|
|
self.heatMap = [0.0] * self.ledCount
|
|
self.gradient = colorArray(COLORS, self._gradient(self.paletteColors,
|
|
102))
|
|
self.frameLen = len(self.gradient)
|
|
self.heatLen = len(self.heatMap)
|
|
self.heatSource = int(self.ledCount / 10.0)
|
|
self.effectRate = int(self.effectRate)
|
|
|
|
if self.heatSource < 1:
|
|
self.heatSource = 1
|
|
|
|
def nextFrame(self, eventtime):
|
|
frame = []
|
|
|
|
for h in range(self.heatLen):
|
|
c = randint(0,self.effectCutoff)
|
|
self.heatMap[h] -= (self.heatMap[h] - c >= 0 ) * c
|
|
|
|
for i in range(self.ledCount - 1, self.heatSource, -1):
|
|
d = (self.heatMap[i - 1] +
|
|
self.heatMap[i - 2] +
|
|
self.heatMap[i - 3] ) / 3
|
|
|
|
self.heatMap[i] = d * (d >= 0)
|
|
|
|
if randint(0, 100) < self.effectRate:
|
|
h = randint(0, self.heatSource)
|
|
self.heatMap[h] += randint(90,100)
|
|
if self.heatMap[h] > 100:
|
|
self.heatMap[h] = 100
|
|
|
|
for h in self.heatMap:
|
|
frame += self.gradient[int(h)]
|
|
|
|
return frame
|
|
|
|
#Fire that responds relative to actual vs target temp
|
|
class layerHeaterFire(_layerBase):
|
|
def __init__(self, **kwargs):
|
|
super(ledEffect.layerHeaterFire, self).__init__(**kwargs)
|
|
|
|
self.heatMap = [0.0] * self.ledCount
|
|
self.gradient = colorArray(COLORS, self._gradient(self.paletteColors,
|
|
102))
|
|
self.frameLen = len(self.gradient)
|
|
self.heatLen = len(self.heatMap)
|
|
self.heatSource = int(self.ledCount / 10.0)
|
|
|
|
if self.handler.heater is None:
|
|
raise self.handler.printer.config_error(
|
|
"LED Effect '%s' has no heater defined." % (self.handler.name))
|
|
|
|
if self.heatSource < 1:
|
|
self.heatSource = 1
|
|
|
|
def nextFrame(self, eventtime):
|
|
frame = []
|
|
spark = 0
|
|
heaterTarget = self.frameHandler.heaterTarget[self.handler.heater]
|
|
heaterCurrent = self.frameHandler.heaterCurrent[self.handler.heater]
|
|
heaterLast = self.frameHandler.heaterLast[self.handler.heater]
|
|
|
|
if heaterTarget > 0.0 and heaterCurrent > 0.0:
|
|
if (heaterCurrent >= self.effectRate):
|
|
if heaterCurrent <= heaterTarget-2:
|
|
spark = int((heaterCurrent / heaterTarget) * 80)
|
|
brightness = int((heaterCurrent / heaterTarget) * 100)
|
|
elif self.effectCutoff > 0:
|
|
spark = 0
|
|
else:
|
|
spark = 80
|
|
brightness = 100
|
|
elif self.effectRate > 0 and heaterCurrent > 0.0:
|
|
if heaterCurrent >= self.effectRate:
|
|
spark = int(((heaterCurrent - self.effectRate)
|
|
/ heaterLast) * 80)
|
|
brightness = int(((heaterCurrent - self.effectRate)
|
|
/ heaterLast) * 100)
|
|
|
|
if spark > 0 and heaterTarget != 0:
|
|
cooling = int((heaterCurrent / heaterTarget) * 20)
|
|
|
|
for h in range(self.heatLen):
|
|
c = randint(0, cooling)
|
|
self.heatMap[h] -= (self.heatMap[h] - c >= 0 ) * c
|
|
|
|
for i in range(self.ledCount - 1, self.heatSource, -1):
|
|
d = (self.heatMap[i - 1] +
|
|
self.heatMap[i - 2] +
|
|
self.heatMap[i - 3] ) / 3
|
|
|
|
self.heatMap[i] = d * (d >= 0)
|
|
|
|
if randint(0, 100) < spark:
|
|
h = randint(0, self.heatSource)
|
|
self.heatMap[h] += brightness
|
|
if self.heatMap[h] > 100:
|
|
self.heatMap[h] = 100
|
|
|
|
for h in self.heatMap:
|
|
frame += self.gradient[int(h)]
|
|
|
|
return frame
|
|
|
|
else:
|
|
return None
|
|
|
|
#Progress bar using M73 gcode command
|
|
class layerProgress(_layerBase):
|
|
def __init__(self, **kwargs):
|
|
super(ledEffect.layerProgress, self).__init__(**kwargs)
|
|
|
|
if self.effectRate < 0:
|
|
self.effectRate = self.ledCount
|
|
|
|
if self.effectCutoff < 0:
|
|
self.effectCutoff = self.ledCount
|
|
|
|
if self.effectRate == 0:
|
|
trailing = colorArray(COLORS, [0.0]*COLORS * self.ledCount)
|
|
else:
|
|
trailing = colorArray(COLORS, self._gradient(self.paletteColors[1:],
|
|
int(self.effectRate), True))
|
|
trailing.padLeft([0.0]*COLORS, self.ledCount)
|
|
|
|
if self.effectCutoff == 0:
|
|
leading = colorArray(COLORS, [0.0]*COLORS * self.ledCount)
|
|
else:
|
|
leading = colorArray(COLORS, self._gradient(self.paletteColors[1:],
|
|
int(self.effectCutoff), False))
|
|
leading.padRight([0.0]*COLORS, self.ledCount)
|
|
|
|
gradient = colorArray(COLORS, trailing + self.paletteColors[0] + leading)
|
|
gradient.shift(len(trailing), 0)
|
|
frames = [gradient[:self.ledCount]]
|
|
|
|
for i in range(0, self.ledCount):
|
|
gradient.shift(1,1)
|
|
frames.append(gradient[:self.ledCount])
|
|
|
|
self.thisFrame.append(colorArray(COLORS, [0.0]*COLORS * self.ledCount))
|
|
for i in range(1, 101):
|
|
x = int((i / 101.0) * self.ledCount)
|
|
self.thisFrame.append(frames[x])
|
|
|
|
self.frameCount = len(self.thisFrame)
|
|
|
|
def nextFrame(self, eventtime):
|
|
p = self.frameHandler.printProgress
|
|
return self.thisFrame[p] #(p - 1) * (p > 0)]
|
|
|
|
class layerStatus(_layerBase):
|
|
def __init__(self, **kwargs):
|
|
super(ledEffect.layerStatus, self).__init__(**kwargs)
|
|
if self.handler.hot is None:
|
|
raise self.handler.printer.config_error(
|
|
"LED Effect '%s' has no heater defined." % (self.handler.name))
|
|
def nextFrame(self, eventtime):
|
|
hotTarget = max(self.frameHandler.hotTarget.values())
|
|
p = self.frameHandler.runStatus
|
|
msg = self.frameHandler.runtStatusMsg
|
|
if p == 'standby':
|
|
color_index = 1 if hotTarget > 0 else 0
|
|
elif p == 'printing':
|
|
color_index = 1
|
|
elif p == 'paused':
|
|
color_index = 2
|
|
elif p == 'cancelled':
|
|
if msg == '':
|
|
color_index = 0
|
|
else:
|
|
color_index = 2
|
|
elif p == 'error':
|
|
color_index = 2
|
|
else:
|
|
color_index = 0
|
|
return self.paletteColors[color_index]
|
|
|
|
class layerHoming(_layerBase):
|
|
def __init__(self, **kwargs):
|
|
super(ledEffect.layerHoming, self).__init__(**kwargs)
|
|
|
|
self.paletteColors = colorArray(COLORS, self.paletteColors)
|
|
|
|
gradientLength = int(self.ledCount)
|
|
gradient = colorArray(COLORS, self._gradient(self.paletteColors,
|
|
gradientLength))
|
|
|
|
for c in range(0, len(self.paletteColors)):
|
|
color = self.paletteColors[c]
|
|
self.thisFrame.append(colorArray(COLORS,color*self.ledCount))
|
|
|
|
self.decayTable = self._decayTable(factor=self.effectRate)
|
|
self.decayTable.append(0.0)
|
|
self.decayLen = len(self.decayTable)
|
|
self.counter=self.decayLen-1
|
|
self.coloridx=-1
|
|
self.my_flag={}
|
|
for endstop in self.handler.endstops:
|
|
self.frameHandler.homing_end_flag[endstop] = 0
|
|
self.my_flag[endstop] = self.frameHandler.homing_end_flag[endstop]
|
|
|
|
def nextFrame(self, eventtime):
|
|
for endstop in self.handler.endstops:
|
|
|
|
if self.my_flag[endstop] != self.frameHandler.homing_end_flag[endstop]:
|
|
self.counter = 0
|
|
self.coloridx = (self.coloridx + 1) % len(self.paletteColors)
|
|
self.my_flag[endstop] = self.frameHandler.homing_end_flag[endstop]
|
|
|
|
frame = [self.decayTable[self.counter] * i for i in self.thisFrame[self.coloridx ]]
|
|
if self.counter < self.decayLen-1:
|
|
self.counter += 1
|
|
|
|
return frame
|
|
class layerSwitchButton(_layerBase):
|
|
def __init__(self, **kwargs):
|
|
super(ledEffect.layerSwitchButton, self).__init__(**kwargs)
|
|
self.last_state = 0
|
|
self.coloridx = 0
|
|
self.fadeValue = 0.0
|
|
self.paletteColors = colorArray(COLORS, self.paletteColors)
|
|
|
|
for c in range(0, len(self.paletteColors)):
|
|
color = self.paletteColors[c]
|
|
self.thisFrame.append(colorArray(COLORS,color*self.ledCount))
|
|
|
|
def nextFrame(self, eventtime):
|
|
if self.handler.button_state > self.last_state:
|
|
self.coloridx = (self.coloridx + 1) % len(self.paletteColors)
|
|
|
|
self.last_state = self.handler.button_state
|
|
|
|
if self.last_state:
|
|
if self.effectRate > 0 and self.fadeValue < 1.0:
|
|
self.fadeValue += (self.handler.frameRate / self.effectRate)
|
|
else:
|
|
self.fadeValue = 1.0
|
|
else:
|
|
if self.effectCutoff > 0 and self.fadeValue > 0.0:
|
|
self.fadeValue -= (self.handler.frameRate / self.effectCutoff)
|
|
else:
|
|
self.fadeValue = 0.0
|
|
|
|
if self.fadeValue < 0: self.fadeValue = 0
|
|
if self.fadeValue > 1.0: self.fadeValue = 1.0
|
|
return [self.fadeValue * i for i in self.thisFrame[self.coloridx]]
|
|
|
|
class layerToggleButton(_layerBase):
|
|
def __init__(self, **kwargs):
|
|
super(ledEffect.layerToggleButton, self).__init__(**kwargs)
|
|
self.last_state = 0
|
|
self.last_coloridx = 0
|
|
self.coloridx = 0
|
|
self.fadeInValue = 0.0
|
|
self.fadeOutValue = 0.0
|
|
self.active = False
|
|
self.paletteColors = colorArray(COLORS, self.paletteColors)
|
|
|
|
for c in range(0, len(self.paletteColors)):
|
|
color = self.paletteColors[c]
|
|
self.thisFrame.append(colorArray(COLORS,color*self.ledCount))
|
|
|
|
def nextFrame(self, eventtime):
|
|
if self.handler.button_state > self.last_state:
|
|
self.last_coloridx = self.coloridx
|
|
self.coloridx = (self.coloridx + 1) % len(self.paletteColors)
|
|
self.last_state = self.handler.button_state
|
|
self.fadeInValue = 0
|
|
self.fadeOutValue = 1.0
|
|
|
|
self.last_state = self.handler.button_state
|
|
|
|
if self.effectRate > 0 and self.fadeInValue < 1.0:
|
|
self.fadeInValue += (self.handler.frameRate / self.effectRate)
|
|
else:
|
|
self.fadeInValue = 1.0
|
|
if self.effectCutoff > 0 and self.fadeOutValue > 0.0:
|
|
self.fadeOutValue -= (self.handler.frameRate / self.effectCutoff)
|
|
else:
|
|
self.fadeOutValue = 0.0
|
|
|
|
if self.fadeInValue < 0: self.fadeInValue = 0
|
|
if self.fadeInValue > 1.0: self.fadeInValue = 1.0
|
|
|
|
if self.fadeOutValue < 0: self.fadeOutValue = 0
|
|
if self.fadeOutValue > 1.0: self.fadeOutValue = 1.0
|
|
|
|
frameIn = [self.fadeInValue * i for i in self.thisFrame[self.coloridx]]
|
|
frameOut = [self.fadeOutValue * i for i in self.thisFrame[self.last_coloridx]]
|
|
|
|
return [ i + o for i, o in zip(frameIn,frameOut)]
|
|
|
|
class layerFlashButton(_layerBase):
|
|
def __init__(self, **kwargs):
|
|
super(ledEffect.layerFlashButton, self).__init__(**kwargs)
|
|
self.last_state = 0
|
|
self.active = False
|
|
self.coloridx = 0
|
|
self.fadeValue = 0.0
|
|
self.paletteColors = colorArray(COLORS, self.paletteColors)
|
|
|
|
for c in range(0, len(self.paletteColors)):
|
|
color = self.paletteColors[c]
|
|
self.thisFrame.append(colorArray(COLORS,color*self.ledCount))
|
|
|
|
def nextFrame(self, eventtime):
|
|
|
|
if self.handler.button_state > self.last_state:
|
|
self.coloridx = (self.coloridx + 1) % len(self.paletteColors)
|
|
self.active = True
|
|
|
|
self.last_state=self.handler.button_state
|
|
|
|
if self.active:
|
|
if self.effectRate > 0 and self.fadeValue < 1.0:
|
|
self.fadeValue += (self.handler.frameRate / self.effectRate)
|
|
else:
|
|
self.fadeValue = 1.0
|
|
if self.fadeValue >= 1.0:
|
|
self.fadeValue = 1.0
|
|
self.active = False
|
|
else:
|
|
if self.effectCutoff > 0 and self.fadeValue > 0.0:
|
|
self.fadeValue -= (self.handler.frameRate / self.effectCutoff)
|
|
else:
|
|
self.fadeValue = 0.0
|
|
|
|
if self.fadeValue <= 0:
|
|
self.fadeValue = 0
|
|
|
|
return [self.fadeValue * i for i in self.thisFrame[self.coloridx]]
|
|
|
|
def load_config_prefix(config):
|
|
return ledEffect(config) |