From 1ab3ac39e410441190690d43f6d2ef339f23a155 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 23 Mar 2022 18:06:09 -0400 Subject: [PATCH] led: Support automated LED updates based on display_template results Signed-off-by: Kevin O'Connor --- docs/Config_Reference.md | 24 ++++++---- docs/G-Codes.md | 17 +++++++ klippy/extras/led.py | 95 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 126 insertions(+), 10 deletions(-) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 1b264801..c62d4274 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -3552,12 +3552,18 @@ text: ### [display_template] Display data text "macros" (one may define any number of sections with -a display_template prefix). This feature allows one to reduce -repetitive definitions in display_data sections. One may use the -builtin render() function in display_data sections to evaluate a -template. For example, if one were to define `[display_template -my_template]` then one could use `{ render('my_template') }` in a -display_data section. +a display_template prefix). See the +[command templates](Command_Templates.md) document for information on +template evaluation. + +This feature allows one to reduce repetitive definitions in +display_data sections. One may use the builtin `render()` function in +display_data sections to evaluate a template. For example, if one were +to define `[display_template my_template]` then one could use `{ +render('my_template') }` in a display_data section. + +This feature can also be used for continuous LED updates using the +[SET_LED_TEMPLATE](G-Codes.md#set_led_template) command. ``` [display_template my_template_name] @@ -3570,9 +3576,9 @@ display_data section. # "param_speed = 75" might have a caller with # "render('my_template_name', param_speed=80)". Parameter names may # not use upper case characters. -#text: -# The text to return when the render() function is called for this -# template. This field is evaluated using command templates (see +text: +# The text to return when the this template is rendered. This field +# is evaluated using command templates (see # docs/Command_Templates.md). This parameter must be provided. ``` diff --git a/docs/G-Codes.md b/docs/G-Codes.md index dbfd0b85..bef64dc4 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -714,6 +714,23 @@ timeout. If careful timing is not needed, the optional SYNC=0 parameter can be specified to apply the changes without resetting the idle timeout. +#### SET_LED_TEMPLATE +`SET_LED_TEMPLATE LED= TEMPLATE= +[INDEX=]`: Assign a +[display_template](Config_Reference.md#display_template) to a given +[LED](Config_Reference.md#leds). For example, if one defined a +`[display_template my_led_template]` config section then one could +assign `TEMPLATE=my_led_template` here. The display_template should +produce a comma separated string containing four floating point +numbers corresponding to red, green, blue, and white color settings. +The template will be continuously evaluated and the LED will be +automatically set to the resulting colors. If INDEX is not specified +then all chips in the LED's daisy-chain will be set to the template, +otherwise only the chip with the given index will be updated. If +TEMPLATE is an empty string then this command will clear any previous +template assigned to the LED (one can then use `SET_LED` commands to +manage the LED's color settings). + ### [output_pin] The following command is available when an diff --git a/klippy/extras/led.py b/klippy/extras/led.py index bfc85956..e77450b4 100644 --- a/klippy/extras/led.py +++ b/klippy/extras/led.py @@ -4,6 +4,10 @@ # # This file may be distributed under the terms of the GNU GPLv3 license. import logging +from .display import display + +# Time between each led template update +RENDER_TIME = 0.500 # Helper code for common LED initialization and control class LEDHelper: @@ -66,8 +70,97 @@ class LEDHelper: class PrinterLED: def __init__(self, config): self.printer = config.get_printer() + self.led_helpers = {} + self.active_templates = {} + self.render_timer = None + # Load templates + dtemplates = display.lookup_display_templates(config) + self.templates = dtemplates.get_display_templates() + gcode_macro = self.printer.lookup_object("gcode_macro") + self.create_template_context = gcode_macro.create_template_context + # Register handlers + gcode = self.printer.lookup_object('gcode') + gcode.register_command("SET_LED_TEMPLATE", self.cmd_SET_LED_TEMPLATE, + desc=self.cmd_SET_LED_TEMPLATE_help) def setup_helper(self, config, update_func, led_count=1, has_white=False): - return LEDHelper(config, update_func, led_count, has_white) + led_helper = LEDHelper(config, update_func, led_count, has_white) + name = config.get_name().split()[-1] + self.led_helpers[name] = led_helper + return led_helper + def _activate_timer(self): + if self.render_timer is not None or not self.active_templates: + return + reactor = self.printer.get_reactor() + self.render_timer = reactor.register_timer(self._render, reactor.NOW) + def _activate_template(self, led_helper, index, template): + key = (led_helper, index) + if template is not None: + self.active_templates[key] = template + return + if key in self.active_templates: + del self.active_templates[key] + def _render(self, eventtime): + if not self.active_templates: + # Nothing to do - unregister timer + reactor = self.printer.get_reactor() + reactor.register_timer(self.render_timer) + self.render_timer = None + return reactor.NEVER + # Setup gcode_macro template context + context = self.create_template_context(eventtime) + def render(name, **kwargs): + return self.templates[name].render(context, **kwargs) + context['render'] = render + # Render all templates + need_transmit = {} + rendered = {} + for (led_helper, index), template in self.active_templates.items(): + color = rendered.get(template) + if color is None: + try: + text = template.render(context) + parts = [max(0., min(1., float(f))) + for f in text.split(',', 4)] + except Exception as e: + logging.exception("led template render error") + parts = [] + if len(parts) < 4: + parts += [0.] * (4 - len(parts)) + rendered[template] = color = tuple(parts) + prev_color = led_helper.led_state[index-1] + if color != prev_color: + if led_helper not in need_transmit: + need_transmit[led_helper] = 1 + led_helper.led_state = list(led_helper.led_state) + led_helper.led_state[index-1] = color + context.clear() # Remove circular references for better gc + # Transmit pending changes + for led_helper in need_transmit.keys(): + try: + led_helper.update_func(led_helper.led_state, None) + except Exception as e: + logging.exception("led template transmit error") + return eventtime + RENDER_TIME + cmd_SET_LED_TEMPLATE_help = "Assign a display_template to an LED" + def cmd_SET_LED_TEMPLATE(self, gcmd): + led_name = gcmd.get("LED") + led_helper = self.led_helpers.get(led_name) + if led_helper is None: + raise gcmd.error("Unknown LED '%s'" % (led_name,)) + led_count = led_helper.led_count + index = gcmd.get_int("INDEX", None, minval=1, maxval=led_count) + template = None + tpl_name = gcmd.get("TEMPLATE") + if tpl_name: + template = self.templates.get(tpl_name) + if template is None: + raise gcmd.error("Unknown display_template '%s'" % (tpl_name,)) + if index is not None: + self._activate_template(led_helper, index, template) + else: + for i in range(led_count): + self._activate_template(led_helper, i+1, template) + self._activate_timer() PIN_MIN_TIME = 0.100 MAX_SCHEDULE_TIME = 5.0