Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for output_pin and fan templates #6695

Merged
merged 5 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/G-Codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,20 @@ enabled.
`SET_FAN_SPEED FAN=config_name SPEED=<speed>` This command sets the
speed of a fan. "speed" must be between 0.0 and 1.0.

`SET_FAN_SPEED PIN=config_name TEMPLATE=<template_name>
[<param_x>=<literal>]`: If `TEMPLATE` is specified then it assigns a
[display_template](Config_Reference.md#display_template) to the given
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps display_template could get a secondary name of just template?
It feels very confusing to be connecting an output pin / fan to a 'display_template' when the intent is to not display anything, but to have it all work in the background...

Maybe dynamic_variable or dynamic_value or something else might be a good secondary name. Just something that doesn't tie it specifically to a 'display' usage.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for reviewing.

I agree that display_template is a confusing name. Roughly I'd like to introduce something like macro_template, make render() always available from Jinja2, and deprecate display_template. That change seems orthogonal though, so didn't want to complicate this PR with a config rename.

-Kevin

fan. For example, if one defined a `[display_template
my_fan_template]` config section then one could assign
`TEMPLATE=my_fan_template` here. The display_template should produce a
string containing a floating point number with the desired value. The
template will be continuously evaluated and the fan will be
automatically set to the resulting speed. One may set display_template
parameters to use during template evaluation (parameters will be
parsed as Python literals). If TEMPLATE is an empty string then this
command will clear any previous template assigned to the pin (one can
then use `SET_FAN_SPEED` commands to manage the values directly).

### [filament_switch_sensor]

The following command is available when a
Expand Down Expand Up @@ -857,6 +871,20 @@ output `VALUE`. VALUE should be 0 or 1 for "digital" output pins. For
PWM pins, set to a value between 0.0 and 1.0, or between 0.0 and
`scale` if a scale is configured in the output_pin config section.

`SET_PIN PIN=config_name TEMPLATE=<template_name> [<param_x>=<literal>]`:
If `TEMPLATE` is specified then it assigns a
[display_template](Config_Reference.md#display_template) to the given
pin. For example, if one defined a `[display_template
my_pin_template]` config section then one could assign
`TEMPLATE=my_pin_template` here. The display_template should produce a
string containing a floating point number with the desired value. The
template will be continuously evaluated and the pin will be
automatically set to the resulting value. One may set display_template
parameters to use during template evaluation (parameters will be
parsed as Python literals). If TEMPLATE is an empty string then this
command will clear any previous template assigned to the pin (one can
then use `SET_PIN` commands to manage the values directly).

### [palette2]

The following commands are available when the
Expand Down
4 changes: 1 addition & 3 deletions klippy/extras/controller_fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,7 @@ def callback(self, eventtime):
self.last_on += 1
if speed != self.last_speed:
self.last_speed = speed
curtime = self.printer.get_reactor().monotonic()
print_time = self.fan.get_mcu().estimated_print_time(curtime)
self.fan.set_speed(print_time + PIN_MIN_TIME, speed)
self.fan.set_speed(speed)
return eventtime + 1.

def load_config_prefix(config):
Expand Down
9 changes: 4 additions & 5 deletions klippy/extras/dotstar.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Support for "dotstar" leds
#
# Copyright (C) 2019-2022 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2019-2024 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
from . import bus
from . import bus, led

BACKGROUND_PRIORITY_CLOCK = 0x7fffffff00000000

Expand All @@ -22,9 +22,8 @@ def __init__(self, config):
self.spi = bus.MCU_SPI(mcu, None, None, 0, 500000, sw_spi_pins)
# Initialize color data
self.chain_count = config.getint('chain_count', 1, minval=1)
pled = printer.load_object(config, "led")
self.led_helper = pled.setup_helper(config, self.update_leds,
self.chain_count)
self.led_helper = led.LEDHelper(config, self.update_leds,
self.chain_count)
self.prev_data = None
# Register commands
printer.register_event_handler("klippy:connect", self.handle_connect)
Expand Down
4 changes: 2 additions & 2 deletions klippy/extras/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ def _apply_speed(self, print_time, value):
return "delay", self.kick_start_time
self.last_fan_value = self.last_req_value = value
self.mcu_fan.set_pwm(print_time, value)
def set_speed(self, print_time, value):
self.gcrq.send_async_request(print_time, value)
def set_speed(self, value, print_time=None):
self.gcrq.send_async_request(value, print_time)
def set_speed_from_command(self, value):
self.gcrq.queue_gcode_request(value)
def _handle_request_restart(self, print_time):
Expand Down
22 changes: 19 additions & 3 deletions klippy/extras/fan_generic.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Support fans that are controlled by gcode
#
# Copyright (C) 2016-2020 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2016-2024 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
from . import fan
from . import fan, output_pin

class PrinterFanGeneric:
cmd_SET_FAN_SPEED_help = "Sets the speed of a fan"
Expand All @@ -12,6 +12,9 @@ def __init__(self, config):
self.fan = fan.Fan(config, default_shutdown_speed=0.)
self.fan_name = config.get_name().split()[-1]

# Template handling
self.template_eval = output_pin.lookup_template_eval(config)

gcode = self.printer.lookup_object("gcode")
gcode.register_mux_command("SET_FAN_SPEED", "FAN",
self.fan_name,
Expand All @@ -20,8 +23,21 @@ def __init__(self, config):

def get_status(self, eventtime):
return self.fan.get_status(eventtime)
def _template_update(self, text):
try:
value = float(text)
except ValueError as e:
logging.exception("fan_generic template render error")
self.fan.set_speed(value)
def cmd_SET_FAN_SPEED(self, gcmd):
speed = gcmd.get_float('SPEED', 0.)
speed = gcmd.get_float('SPEED', None, 0.)
template = gcmd.get('TEMPLATE', None)
if (speed is None) == (template is None):
raise gcmd.error("SET_FAN_SPEED must specify SPEED or TEMPLATE")
# Check for template setting
if template is not None:
self.template_eval.set_template(gcmd, self._template_update)
return
self.fan.set_speed_from_command(speed)

def load_config_prefix(config):
Expand Down
4 changes: 1 addition & 3 deletions klippy/extras/heater_fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@ def callback(self, eventtime):
speed = self.fan_speed
if speed != self.last_speed:
self.last_speed = speed
curtime = self.printer.get_reactor().monotonic()
print_time = self.fan.get_mcu().estimated_print_time(curtime)
self.fan.set_speed(print_time + PIN_MIN_TIME, speed)
self.fan.set_speed(speed)
return eventtime + 1.

def load_config_prefix(config):
Expand Down
152 changes: 33 additions & 119 deletions klippy/extras/led.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
# Support for PWM driven LEDs
#
# Copyright (C) 2019-2022 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2019-2024 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging, ast
from .display import display

# Time between each led template update
RENDER_TIME = 0.500
import logging
from . import output_pin

# Helper code for common LED initialization and control
class LEDHelper:
Expand All @@ -22,14 +19,22 @@ def __init__(self, config, update_func, led_count=1):
blue = config.getfloat('initial_BLUE', 0., minval=0., maxval=1.)
white = config.getfloat('initial_WHITE', 0., minval=0., maxval=1.)
self.led_state = [(red, green, blue, white)] * led_count
# Support setting an led template
self.template_eval = output_pin.lookup_template_eval(config)
self.tcallbacks = [(lambda text, s=self, index=i:
s._template_update(index, text))
for i in range(led_count)]
# Register commands
name = config.get_name().split()[-1]
gcode = self.printer.lookup_object('gcode')
gcode.register_mux_command("SET_LED", "LED", name, self.cmd_SET_LED,
desc=self.cmd_SET_LED_help)
def get_led_count(self):
return self.led_count
def set_color(self, index, color):
gcode.register_mux_command("SET_LED_TEMPLATE", "LED", name,
self.cmd_SET_LED_TEMPLATE,
desc=self.cmd_SET_LED_TEMPLATE_help)
def get_status(self, eventtime=None):
return {'color_data': self.led_state}
def _set_color(self, index, color):
if index is None:
new_led_state = [color] * self.led_count
if self.led_state == new_led_state:
Expand All @@ -41,7 +46,17 @@ def set_color(self, index, color):
new_led_state[index - 1] = color
self.led_state = new_led_state
self.need_transmit = True
def check_transmit(self, print_time):
def _template_update(self, index, text):
try:
parts = [max(0., min(1., float(f)))
for f in text.split(',', 4)]
except ValueError as e:
logging.exception("led template render error")
parts = []
if len(parts) < 4:
parts += [0.] * (4 - len(parts))
self._set_color(index, tuple(parts))
def _check_transmit(self, print_time=None):
if not self.need_transmit:
return
self.need_transmit = False
Expand All @@ -62,122 +77,25 @@ def cmd_SET_LED(self, gcmd):
color = (red, green, blue, white)
# Update and transmit data
def lookahead_bgfunc(print_time):
self.set_color(index, color)
self._set_color(index, color)
if transmit:
self.check_transmit(print_time)
self._check_transmit(print_time)
if sync:
#Sync LED Update with print time and send
toolhead = self.printer.lookup_object('toolhead')
toolhead.register_lookahead_callback(lookahead_bgfunc)
else:
#Send update now (so as not to wake toolhead and reset idle_timeout)
lookahead_bgfunc(None)
def get_status(self, eventtime=None):
return {'color_data': self.led_state}

# Main LED tracking code
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):
led_helper = LEDHelper(config, update_func, led_count)
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, lparams):
key = (led_helper, index)
if template is not None:
uid = (template,) + tuple(sorted(lparams.items()))
self.active_templates[key] = (uid, template, lparams)
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.unregister_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 = {}
template_info = self.active_templates.items()
for (led_helper, index), (uid, template, lparams) in template_info:
color = rendered.get(uid)
if color is None:
try:
text = template.render(context, **lparams)
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[uid] = color = tuple(parts)
need_transmit[led_helper] = 1
led_helper.set_color(index, color)
context.clear() # Remove circular references for better gc
# Transmit pending changes
for led_helper in need_transmit.keys():
led_helper.check_transmit(None)
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.get_led_count()
index = gcmd.get_int("INDEX", None, minval=1, maxval=led_count)
template = None
lparams = {}
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,))
tparams = template.get_params()
for p, v in gcmd.get_command_parameters().items():
if not p.startswith("PARAM_"):
continue
p = p.lower()
if p not in tparams:
raise gcmd.error("Invalid display_template parameter: %s"
% (p,))
try:
lparams[p] = ast.literal_eval(v)
except ValueError as e:
raise gcmd.error("Unable to parse '%s' as a literal" % (v,))
index = gcmd.get_int("INDEX", None, minval=1, maxval=self.led_count)
set_template = self.template_eval.set_template
if index is not None:
self._activate_template(led_helper, index, template, lparams)
set_template(gcmd, self.tcallbacks[index-1], self._check_transmit)
else:
for i in range(led_count):
self._activate_template(led_helper, i+1, template, lparams)
self._activate_timer()
for i in range(self.led_count):
set_template(gcmd, self.tcallbacks[i], self._check_transmit)

PIN_MIN_TIME = 0.100
MAX_SCHEDULE_TIME = 5.0
Expand Down Expand Up @@ -205,8 +123,7 @@ def __init__(self, config):
% (config.get_name(),))
self.last_print_time = 0.
# Initialize color data
pled = printer.load_object(config, "led")
self.led_helper = pled.setup_helper(config, self.update_leds, 1)
self.led_helper = LEDHelper(config, self.update_leds, 1)
self.prev_color = color = self.led_helper.get_status()['color_data'][0]
for idx, mcu_pin in self.pins:
mcu_pin.setup_start_value(color[idx], 0.)
Expand All @@ -225,8 +142,5 @@ def update_leds(self, led_state, print_time):
def get_status(self, eventtime=None):
return self.led_helper.get_status(eventtime)

def load_config(config):
return PrinterLED(config)

def load_config_prefix(config):
return PrinterPWMLED(config)
5 changes: 2 additions & 3 deletions klippy/extras/neopixel.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
from . import led

BACKGROUND_PRIORITY_CLOCK = 0x7fffffff00000000

Expand Down Expand Up @@ -40,9 +41,7 @@ def __init__(self, config):
if len(self.color_map) > MAX_MCU_SIZE:
raise config.error("neopixel chain too long")
# Initialize color data
pled = printer.load_object(config, "led")
self.led_helper = pled.setup_helper(config, self.update_leds,
chain_count)
self.led_helper = led.LEDHelper(config, self.update_leds, chain_count)
self.color_data = bytearray(len(self.color_map))
self.update_color_data(self.led_helper.get_status()['color_data'])
self.old_color_data = bytearray([d ^ 1 for d in self.color_data])
Expand Down
Loading
Loading