From cdc71daaa0f3ebfa648bf7038838c02263faaf46 Mon Sep 17 00:00:00 2001 From: Pascal Pieper Date: Fri, 26 Feb 2021 18:20:21 +0100 Subject: [PATCH 01/14] Added 'tool' module to control PWM-tools like lasers or spindles via M3-M5 Signed-off-by: Pascal Pieper --- docs/Tool.md | 37 +++++++++++++++++++ klippy/extras/tool.py | 86 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 docs/Tool.md create mode 100644 klippy/extras/tool.py diff --git a/docs/Tool.md b/docs/Tool.md new file mode 100644 index 000000000000..e2da6bba5420 --- /dev/null +++ b/docs/Tool.md @@ -0,0 +1,37 @@ +This document describes the _tool_ module. + + +## How does it work? +This enables the use of _M3 M4 M5_ GCODE commands. +It is based on the generic fan PWM module, but adds a shutdown value +and makes use of the MCU's safety timeout. + +## Configuration + [tool] + pin: !ar9 + + hardware_pwm: True + # Default: True + + cycle_time: 0.001 + # Default: 0.01 + + max_power: 1 + # Default: 1 + + off_below: 0 + # Default: 0 + + shutdown_value: 0 + # Default: 0 + + safety_timeout: 20 + # Default: 0 (disabled) + # Suggested value is a minimal amount longer + # than the longest running command. + # Currently, moves are not split into individual sections. + +## Commands + +`M3/M4 S` : Set PWM duty-cycle. Values between 0 and 255. +`M5` : Stop PWM output to shutdown value. diff --git a/klippy/extras/tool.py b/klippy/extras/tool.py new file mode 100644 index 000000000000..222c6f56ea1f --- /dev/null +++ b/klippy/extras/tool.py @@ -0,0 +1,86 @@ +# Printer cooling fan +# +# This file may be distributed under the terms of the GNU GPLv3 license. + +class PWM_tool: + def __init__(self, config, default_shutdown_value=0.): + self.printer = config.get_printer() + self.last_value = 0. + self.last_time = 0. + self.safety_timeout = config.getfloat('safety_timeout', 5, + minval=0.) + self.reactor = self.printer.get_reactor() + self.resend_timer = self.reactor.register_timer( + self._resend_current_val) + # Read config + self.max_value = config.getfloat('max_power', 1., above=0., maxval=1.) + self.off_below = config.getfloat('off_below', default=0., + minval=0., maxval=1.) + cycle_time = config.getfloat('cycle_time', 0.01, above=0.) + hardware_pwm = config.getboolean('hardware_pwm', True) + shutdown_value = config.getfloat( + 'shutdown_value', default_shutdown_value, minval=0., maxval=1.) + # Setup pwm object + ppins = self.printer.lookup_object('pins') + self.mcu_pwm = ppins.setup_pin('pwm', config.get('pin')) + self.mcu_pwm.setup_max_duration(self.safety_timeout) + self.mcu_pwm.setup_cycle_time(cycle_time, hardware_pwm) + self.shutdown_value = max(0., min(self.max_value, shutdown_value)) + self.mcu_pwm.setup_start_value(0., shutdown_value) + # Register callbacks + self.printer.register_event_handler("gcode:request_restart", + self._handle_request_restart) + def get_mcu(self): + return self.mcu_pwm.get_mcu() + def set_value(self, print_time, value, resend=False): + if value < self.off_below: + value = 0. + value = max(0., min(self.max_value, value * self.max_value)) + if value == self.last_value and not resend: + return + print_time = max(self.last_time, print_time) + self.last_time = print_time + self.last_value = value + self.mcu_pwm.set_pwm(print_time, value) + if self.safety_timeout != 0 and not resend: + if value != self.shutdown_value: + self.reactor.update_timer( + self.resend_timer, + self.reactor.monotonic() + 0.75 * self.safety_timeout) + def set_value_from_command(self, value): + toolhead = self.printer.lookup_object('toolhead') + toolhead.register_lookahead_callback((lambda pt: + self.set_value(pt, value))) + def _handle_request_restart(self, print_time): + self.set_value(print_time, 0.) + def get_status(self, eventtime): + return {'value': self.last_value} + def _resend_current_val(self, eventtime): + # TODO: Split moves into smaller segments to enforce resend + toolhead = self.printer.lookup_object('toolhead') + toolhead.register_lookahead_callback((lambda pt: + self.set_value(pt, self.last_value, True))) + if self.last_value != self.shutdown_value: + return eventtime + 0.75 * self.safety_timeout + return self.reactor.NEVER + +class PrinterSpindle: + def __init__(self, config): + self.tool = PWM_tool(config) + # Register commands + gcode = config.get_printer().lookup_object('gcode') + gcode.register_command("M3", self.cmd_M3_M4) + gcode.register_command("M4", self.cmd_M3_M4) + gcode.register_command("M5", self.cmd_M5) + def get_status(self, eventtime): + return self.tool.get_status(eventtime) + def cmd_M3_M4(self, gcmd): + # Set spindle speed + value = gcmd.get_float('S', 255., minval=0.) / 255. + self.tool.set_value_from_command(value) + def cmd_M5(self, gcmd): + # Turn spindle off + self.tool.set_value_from_command(0.) + +def load_config(config): + return PrinterSpindle(config) From cda450efcd0ae6ad20ff2a41cd1ff4114f31131c Mon Sep 17 00:00:00 2001 From: Pascal Pieper Date: Sun, 7 Mar 2021 11:33:09 +0100 Subject: [PATCH 02/14] Moved timeout functionality to output_pin Signed-off-by: Pascal Pieper --- klippy/extras/output_pin.py | 35 ++++++++++++++++++++++++++++++----- klippy/extras/tool.py | 5 ++++- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/klippy/extras/output_pin.py b/klippy/extras/output_pin.py index f9a886f6d6a0..8c9cf9147bdc 100644 --- a/klippy/extras/output_pin.py +++ b/klippy/extras/output_pin.py @@ -22,20 +22,27 @@ def __init__(self, config): self.mcu_pin = ppins.setup_pin('digital_out', config.get('pin')) self.scale = 1. self.last_cycle_time = self.default_cycle_time = 0. - self.mcu_pin.setup_max_duration(0.) self.last_print_time = 0. static_value = config.getfloat('static_value', None, minval=0., maxval=self.scale) if static_value is not None: + self.mcu_pin.setup_max_duration(0.) self.last_value = static_value / self.scale self.mcu_pin.setup_start_value( self.last_value, self.last_value, True) else: + self.reactor = self.printer.get_reactor() + self.safety_timeout = config.getfloat('safety_timeout', 5, + minval=0.) + self.mcu_pin.setup_max_duration(self.safety_timeout) + self.resend_timer = self.reactor.register_timer( + self._resend_current_val) + self.last_value = config.getfloat( 'value', 0., minval=0., maxval=self.scale) / self.scale - shutdown_value = config.getfloat( + self.shutdown_value = config.getfloat( 'shutdown_value', 0., minval=0., maxval=self.scale) / self.scale - self.mcu_pin.setup_start_value(self.last_value, shutdown_value) + self.mcu_pin.setup_start_value(self.last_value, self.shutdown_value) pin_name = config.get_name().split()[1] gcode = self.printer.lookup_object('gcode') gcode.register_mux_command("SET_PIN", "PIN", pin_name, @@ -43,9 +50,10 @@ def __init__(self, config): desc=self.cmd_SET_PIN_help) def get_status(self, eventtime): return {'value': self.last_value} - def _set_pin(self, print_time, value, cycle_time): + def _set_pin(self, print_time, value, cycle_time, is_resend=False): if value == self.last_value and cycle_time == self.last_cycle_time: - return + if not is_resend: + return print_time = max(print_time, self.last_print_time + PIN_MIN_TIME) if self.is_pwm: self.mcu_pin.set_pwm(print_time, value, cycle_time) @@ -54,6 +62,13 @@ def _set_pin(self, print_time, value, cycle_time): self.last_value = value self.last_cycle_time = cycle_time self.last_print_time = print_time + if self.safety_timeout != 0 and not is_resend: + if value == self.shutdown_value: + self.reactor.update_timer( + self.resend_timer, self.reactor.NEVER) + else: + self.reactor.update_timer( + self.resend_timer, self.reactor.monotonic()) cmd_SET_PIN_help = "Set the value of an output pin" def cmd_SET_PIN(self, gcmd): value = gcmd.get_float('VALUE', minval=0., maxval=self.scale) @@ -66,5 +81,15 @@ def cmd_SET_PIN(self, gcmd): toolhead.register_lookahead_callback( lambda print_time: self._set_pin(print_time, value, cycle_time)) + def _resend_current_val(self, eventtime): + toolhead = self.printer.lookup_object('toolhead') + toolhead.register_lookahead_callback(lambda print_time: + self._set_pin(self.last_print_time + 0.8 * self.safety_timeout, + self.last_value, self.last_cycle_time, True) + ) + if self.last_value != self.shutdown_value: + return eventtime + 0.7 * self.safety_timeout + return self.reactor.NEVER + def load_config_prefix(config): return PrinterOutputPin(config) diff --git a/klippy/extras/tool.py b/klippy/extras/tool.py index 222c6f56ea1f..b058bcd8ff6c 100644 --- a/klippy/extras/tool.py +++ b/klippy/extras/tool.py @@ -43,7 +43,10 @@ def set_value(self, print_time, value, resend=False): self.last_value = value self.mcu_pwm.set_pwm(print_time, value) if self.safety_timeout != 0 and not resend: - if value != self.shutdown_value: + if value == self.shutdown_value: + self.reactor.update_timer( + self.resend_timer, self.reactor.NEVER) + else: self.reactor.update_timer( self.resend_timer, self.reactor.monotonic() + 0.75 * self.safety_timeout) From 3ba62ab8da51ead1bdcee20eeed128fc8d36bc56 Mon Sep 17 00:00:00 2001 From: Pascal Pieper Date: Sun, 7 Mar 2021 13:08:39 +0100 Subject: [PATCH 03/14] Added documentation for using powered tools Signed-off-by: Pascal Pieper --- docs/Config_Reference.md | 7 +++ docs/Tool.md | 37 --------------- docs/Using_PWM_Tools.md | 48 ++++++++++++++++++++ klippy/extras/output_pin.py | 2 +- klippy/extras/tool.py | 89 ------------------------------------- 5 files changed, 56 insertions(+), 127 deletions(-) delete mode 100644 docs/Tool.md create mode 100644 docs/Using_PWM_Tools.md delete mode 100644 klippy/extras/tool.py diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 04030887eaf7..d3e2e0eb1151 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -2484,6 +2484,13 @@ pin: #shutdown_value: # The value to set the pin to on an MCU shutdown event. The default # is 0 (for low voltage). +#safety_timeout: +# The maximum duration a pin may be driven by the MCU +# without an update from the host. +# If host can not keep up with an update, the MCU will shutdown +# and set all pins to their respective shutdown values. +# Default: 0 (disabled) +# Usual values are around 5 seconds. #cycle_time: 0.100 # The amount of time (in seconds) per PWM cycle. It is recommended # this be 10 milliseconds or greater when using software based PWM. diff --git a/docs/Tool.md b/docs/Tool.md deleted file mode 100644 index e2da6bba5420..000000000000 --- a/docs/Tool.md +++ /dev/null @@ -1,37 +0,0 @@ -This document describes the _tool_ module. - - -## How does it work? -This enables the use of _M3 M4 M5_ GCODE commands. -It is based on the generic fan PWM module, but adds a shutdown value -and makes use of the MCU's safety timeout. - -## Configuration - [tool] - pin: !ar9 - - hardware_pwm: True - # Default: True - - cycle_time: 0.001 - # Default: 0.01 - - max_power: 1 - # Default: 1 - - off_below: 0 - # Default: 0 - - shutdown_value: 0 - # Default: 0 - - safety_timeout: 20 - # Default: 0 (disabled) - # Suggested value is a minimal amount longer - # than the longest running command. - # Currently, moves are not split into individual sections. - -## Commands - -`M3/M4 S` : Set PWM duty-cycle. Values between 0 and 255. -`M5` : Stop PWM output to shutdown value. diff --git a/docs/Using_PWM_Tools.md b/docs/Using_PWM_Tools.md new file mode 100644 index 000000000000..f84394116fe6 --- /dev/null +++ b/docs/Using_PWM_Tools.md @@ -0,0 +1,48 @@ +This document describes how to setup a PWM-Controlled laser or spindle +using `output_pin` and some macros. + + +## How does it work? +With re-purposing the printhead's fan pwm output, you can control +lasers or spindles. +Usually, cam-tools such as LaserWeb can be configured to use `M3-M5` +commands, which stand for _spindle speed CW_ (`M3 S[0-255]`), +_spindle speed CCW_ (`M4 S[0-255]`) and _spindle stop_ (`M5`). + +For safety reasons, you should configure a safety timeout, +so that when your host or MCU encounters an error, the tool will stop. + + +## Configuration + [output_pin TOOL] + pin: !ar9 + pwm: True + hardware_pwm: False + cycle_time: 0.001 + shutdown_value: 0 + + safety_timeout: 5 + # Default: 0 (disabled) + # Suggested value is around 5 seconds. + # Use a value that does not burn up your stock. + # Please note that during homing, your tool + # needs to be in default speed. + + [gcode_macro M3] + default_parameter_S: 0 + gcode: + SET_PIN PIN=TOOL VALUE={S|float / 255} + + [gcode_macro M4] + default_parameter_S: 0 + gcode: + SET_PIN PIN=TOOL VALUE={S|float / 255} + + [gcode_macro M5] + gcode: + SET_PIN PIN=TOOL VALUE=0 + +## Commands + +`M3/M4 S` : Set PWM duty-cycle. Values between 0 and 255. +`M5` : Stop PWM output to shutdown value. diff --git a/klippy/extras/output_pin.py b/klippy/extras/output_pin.py index 8c9cf9147bdc..ca60c89f1636 100644 --- a/klippy/extras/output_pin.py +++ b/klippy/extras/output_pin.py @@ -32,7 +32,7 @@ def __init__(self, config): self.last_value, self.last_value, True) else: self.reactor = self.printer.get_reactor() - self.safety_timeout = config.getfloat('safety_timeout', 5, + self.safety_timeout = config.getfloat('safety_timeout', 0, minval=0.) self.mcu_pin.setup_max_duration(self.safety_timeout) self.resend_timer = self.reactor.register_timer( diff --git a/klippy/extras/tool.py b/klippy/extras/tool.py deleted file mode 100644 index b058bcd8ff6c..000000000000 --- a/klippy/extras/tool.py +++ /dev/null @@ -1,89 +0,0 @@ -# Printer cooling fan -# -# This file may be distributed under the terms of the GNU GPLv3 license. - -class PWM_tool: - def __init__(self, config, default_shutdown_value=0.): - self.printer = config.get_printer() - self.last_value = 0. - self.last_time = 0. - self.safety_timeout = config.getfloat('safety_timeout', 5, - minval=0.) - self.reactor = self.printer.get_reactor() - self.resend_timer = self.reactor.register_timer( - self._resend_current_val) - # Read config - self.max_value = config.getfloat('max_power', 1., above=0., maxval=1.) - self.off_below = config.getfloat('off_below', default=0., - minval=0., maxval=1.) - cycle_time = config.getfloat('cycle_time', 0.01, above=0.) - hardware_pwm = config.getboolean('hardware_pwm', True) - shutdown_value = config.getfloat( - 'shutdown_value', default_shutdown_value, minval=0., maxval=1.) - # Setup pwm object - ppins = self.printer.lookup_object('pins') - self.mcu_pwm = ppins.setup_pin('pwm', config.get('pin')) - self.mcu_pwm.setup_max_duration(self.safety_timeout) - self.mcu_pwm.setup_cycle_time(cycle_time, hardware_pwm) - self.shutdown_value = max(0., min(self.max_value, shutdown_value)) - self.mcu_pwm.setup_start_value(0., shutdown_value) - # Register callbacks - self.printer.register_event_handler("gcode:request_restart", - self._handle_request_restart) - def get_mcu(self): - return self.mcu_pwm.get_mcu() - def set_value(self, print_time, value, resend=False): - if value < self.off_below: - value = 0. - value = max(0., min(self.max_value, value * self.max_value)) - if value == self.last_value and not resend: - return - print_time = max(self.last_time, print_time) - self.last_time = print_time - self.last_value = value - self.mcu_pwm.set_pwm(print_time, value) - if self.safety_timeout != 0 and not resend: - if value == self.shutdown_value: - self.reactor.update_timer( - self.resend_timer, self.reactor.NEVER) - else: - self.reactor.update_timer( - self.resend_timer, - self.reactor.monotonic() + 0.75 * self.safety_timeout) - def set_value_from_command(self, value): - toolhead = self.printer.lookup_object('toolhead') - toolhead.register_lookahead_callback((lambda pt: - self.set_value(pt, value))) - def _handle_request_restart(self, print_time): - self.set_value(print_time, 0.) - def get_status(self, eventtime): - return {'value': self.last_value} - def _resend_current_val(self, eventtime): - # TODO: Split moves into smaller segments to enforce resend - toolhead = self.printer.lookup_object('toolhead') - toolhead.register_lookahead_callback((lambda pt: - self.set_value(pt, self.last_value, True))) - if self.last_value != self.shutdown_value: - return eventtime + 0.75 * self.safety_timeout - return self.reactor.NEVER - -class PrinterSpindle: - def __init__(self, config): - self.tool = PWM_tool(config) - # Register commands - gcode = config.get_printer().lookup_object('gcode') - gcode.register_command("M3", self.cmd_M3_M4) - gcode.register_command("M4", self.cmd_M3_M4) - gcode.register_command("M5", self.cmd_M5) - def get_status(self, eventtime): - return self.tool.get_status(eventtime) - def cmd_M3_M4(self, gcmd): - # Set spindle speed - value = gcmd.get_float('S', 255., minval=0.) / 255. - self.tool.set_value_from_command(value) - def cmd_M5(self, gcmd): - # Turn spindle off - self.tool.set_value_from_command(0.) - -def load_config(config): - return PrinterSpindle(config) From 40acc7c1f327549c3b6f2b5ef39f4b4d8640474c Mon Sep 17 00:00:00 2001 From: Pascal Pieper Date: Mon, 15 Mar 2021 13:24:09 +0100 Subject: [PATCH 04/14] Testing run without register_lookahead_callback Signed-off-by: Pascal Pieper --- klippy/extras/output_pin.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/klippy/extras/output_pin.py b/klippy/extras/output_pin.py index ca60c89f1636..647497924a22 100644 --- a/klippy/extras/output_pin.py +++ b/klippy/extras/output_pin.py @@ -6,6 +6,7 @@ PIN_MIN_TIME = 0.100 + class PrinterOutputPin: def __init__(self, config): self.printer = config.get_printer() @@ -34,6 +35,7 @@ def __init__(self, config): self.reactor = self.printer.get_reactor() self.safety_timeout = config.getfloat('safety_timeout', 0, minval=0.) + self.relax_margin = 0.1 * self.safety_timeout + PIN_MIN_TIME self.mcu_pin.setup_max_duration(self.safety_timeout) self.resend_timer = self.reactor.register_timer( self._resend_current_val) @@ -82,13 +84,11 @@ def cmd_SET_PIN(self, gcmd): lambda print_time: self._set_pin(print_time, value, cycle_time)) def _resend_current_val(self, eventtime): - toolhead = self.printer.lookup_object('toolhead') - toolhead.register_lookahead_callback(lambda print_time: - self._set_pin(self.last_print_time + 0.8 * self.safety_timeout, - self.last_value, self.last_cycle_time, True) - ) + self._set_pin(self.last_print_time + 0.8 * self.safety_timeout, + self.last_value, self.last_cycle_time, True) + if self.last_value != self.shutdown_value: - return eventtime + 0.7 * self.safety_timeout + return eventtime + (0.8 * self.safety_timeout) - self.relax_margin return self.reactor.NEVER def load_config_prefix(config): From eadd811651f80c3f52c993815af6166a34906f53 Mon Sep 17 00:00:00 2001 From: Pascal Pieper Date: Mon, 15 Mar 2021 13:55:59 +0100 Subject: [PATCH 05/14] Keep constant time offset, dont accumulate margin Signed-off-by: Pascal Pieper --- klippy/extras/output_pin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/klippy/extras/output_pin.py b/klippy/extras/output_pin.py index 647497924a22..eb33ec79946d 100644 --- a/klippy/extras/output_pin.py +++ b/klippy/extras/output_pin.py @@ -84,7 +84,8 @@ def cmd_SET_PIN(self, gcmd): lambda print_time: self._set_pin(print_time, value, cycle_time)) def _resend_current_val(self, eventtime): - self._set_pin(self.last_print_time + 0.8 * self.safety_timeout, + print_time = self.mcu_pwm.get_mcu().estimated_print_time(eventtime) + self._set_pin(print_time + self.relax_margin, self.last_value, self.last_cycle_time, True) if self.last_value != self.shutdown_value: From 8ba87dd4c2929f6e321469700b55389cf9ebc397 Mon Sep 17 00:00:00 2001 From: Pascal Pieper Date: Mon, 15 Mar 2021 14:14:09 +0100 Subject: [PATCH 06/14] Fixup: mcu_pin, not mcu_pwm Signed-off-by: Pascal Pieper --- klippy/extras/output_pin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/extras/output_pin.py b/klippy/extras/output_pin.py index eb33ec79946d..cff6f3f1f92f 100644 --- a/klippy/extras/output_pin.py +++ b/klippy/extras/output_pin.py @@ -84,7 +84,7 @@ def cmd_SET_PIN(self, gcmd): lambda print_time: self._set_pin(print_time, value, cycle_time)) def _resend_current_val(self, eventtime): - print_time = self.mcu_pwm.get_mcu().estimated_print_time(eventtime) + print_time = self.mcu_pin.get_mcu().estimated_print_time(eventtime) self._set_pin(print_time + self.relax_margin, self.last_value, self.last_cycle_time, True) From c57abb6873da72bf2cc5f98d2ac21e796bdc2752 Mon Sep 17 00:00:00 2001 From: Pascal Pieper Date: Mon, 15 Mar 2021 17:05:14 +0100 Subject: [PATCH 07/14] Reduced command wait duration Signed-off-by: Pascal Pieper --- klippy/extras/output_pin.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/klippy/extras/output_pin.py b/klippy/extras/output_pin.py index cff6f3f1f92f..735750e16614 100644 --- a/klippy/extras/output_pin.py +++ b/klippy/extras/output_pin.py @@ -35,7 +35,8 @@ def __init__(self, config): self.reactor = self.printer.get_reactor() self.safety_timeout = config.getfloat('safety_timeout', 0, minval=0.) - self.relax_margin = 0.1 * self.safety_timeout + PIN_MIN_TIME + #ensure that safety timeout is big enough for comm. latency + self.safety_timeout = max(self.safety_timeout, 1.25*PIN_MIN_TIME) self.mcu_pin.setup_max_duration(self.safety_timeout) self.resend_timer = self.reactor.register_timer( self._resend_current_val) @@ -85,11 +86,11 @@ def cmd_SET_PIN(self, gcmd): def _resend_current_val(self, eventtime): print_time = self.mcu_pin.get_mcu().estimated_print_time(eventtime) - self._set_pin(print_time + self.relax_margin, + self._set_pin(print_time + PIN_MIN_TIME, self.last_value, self.last_cycle_time, True) if self.last_value != self.shutdown_value: - return eventtime + (0.8 * self.safety_timeout) - self.relax_margin + return eventtime + (0.8 * self.safety_timeout) - PIN_MIN_TIME return self.reactor.NEVER def load_config_prefix(config): From 6838763d212619c59eb30e0ec68c80a26b391d4e Mon Sep 17 00:00:00 2001 From: Pascal Pieper Date: Mon, 15 Mar 2021 17:26:15 +0100 Subject: [PATCH 08/14] Renamed timeout Signed-off-by: Pascal Pieper --- docs/Config_Reference.md | 6 +++--- docs/Using_PWM_Tools.md | 4 +++- klippy/extras/output_pin.py | 13 +++++++------ 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index d3e2e0eb1151..f4a67071a181 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -2484,9 +2484,9 @@ pin: #shutdown_value: # The value to set the pin to on an MCU shutdown event. The default # is 0 (for low voltage). -#safety_timeout: -# The maximum duration a pin may be driven by the MCU -# without an update from the host. +#host_acknowledge_timeout: +# The maximum duration a non-shutdown value may be driven by the MCU +# without an acknowledge from the host. # If host can not keep up with an update, the MCU will shutdown # and set all pins to their respective shutdown values. # Default: 0 (disabled) diff --git a/docs/Using_PWM_Tools.md b/docs/Using_PWM_Tools.md index f84394116fe6..46f41ff5d816 100644 --- a/docs/Using_PWM_Tools.md +++ b/docs/Using_PWM_Tools.md @@ -21,8 +21,10 @@ so that when your host or MCU encounters an error, the tool will stop. cycle_time: 0.001 shutdown_value: 0 - safety_timeout: 5 + host_acknowledge_timeout: 5 # Default: 0 (disabled) + # Amount of time in which the host has to acknowledge + # a non-shutdown output value. # Suggested value is around 5 seconds. # Use a value that does not burn up your stock. # Please note that during homing, your tool diff --git a/klippy/extras/output_pin.py b/klippy/extras/output_pin.py index 735750e16614..158ffb57eac7 100644 --- a/klippy/extras/output_pin.py +++ b/klippy/extras/output_pin.py @@ -33,11 +33,12 @@ def __init__(self, config): self.last_value, self.last_value, True) else: self.reactor = self.printer.get_reactor() - self.safety_timeout = config.getfloat('safety_timeout', 0, - minval=0.) + self.host_ack_timeout = config.getfloat('host_acknowledge_timeout', + 0, minval=0.) #ensure that safety timeout is big enough for comm. latency - self.safety_timeout = max(self.safety_timeout, 1.25*PIN_MIN_TIME) - self.mcu_pin.setup_max_duration(self.safety_timeout) + if self.host_ack_timeout > 0: + self.host_ack_timeout = max(self.host_ack_timeout, 0.500) + self.mcu_pin.setup_max_duration(self.host_ack_timeout) self.resend_timer = self.reactor.register_timer( self._resend_current_val) @@ -65,7 +66,7 @@ def _set_pin(self, print_time, value, cycle_time, is_resend=False): self.last_value = value self.last_cycle_time = cycle_time self.last_print_time = print_time - if self.safety_timeout != 0 and not is_resend: + if self.host_ack_timeout != 0 and not is_resend: if value == self.shutdown_value: self.reactor.update_timer( self.resend_timer, self.reactor.NEVER) @@ -90,7 +91,7 @@ def _resend_current_val(self, eventtime): self.last_value, self.last_cycle_time, True) if self.last_value != self.shutdown_value: - return eventtime + (0.8 * self.safety_timeout) - PIN_MIN_TIME + return eventtime + (0.8 * self.host_ack_timeout) - PIN_MIN_TIME return self.reactor.NEVER def load_config_prefix(config): From 87a57e4adebda103532845529d9e1f8fb13c63ab Mon Sep 17 00:00:00 2001 From: Pascal Pieper Date: Tue, 16 Mar 2021 17:13:53 +0100 Subject: [PATCH 09/14] First update needs to be scheduled into the future Signed-off-by: Pascal Pieper --- klippy/extras/output_pin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/klippy/extras/output_pin.py b/klippy/extras/output_pin.py index 158ffb57eac7..9619c82fe2f4 100644 --- a/klippy/extras/output_pin.py +++ b/klippy/extras/output_pin.py @@ -72,7 +72,8 @@ def _set_pin(self, print_time, value, cycle_time, is_resend=False): self.resend_timer, self.reactor.NEVER) else: self.reactor.update_timer( - self.resend_timer, self.reactor.monotonic()) + self.resend_timer, self.reactor.monotonic() + + (0.8 * self.host_ack_timeout) - PIN_MIN_TIME) cmd_SET_PIN_help = "Set the value of an output pin" def cmd_SET_PIN(self, gcmd): value = gcmd.get_float('VALUE', minval=0., maxval=self.scale) From 7b9dd0e07259eff11e20baf9d9e769556adf8fb2 Mon Sep 17 00:00:00 2001 From: Pascal Pieper Date: Wed, 17 Mar 2021 18:19:02 +0100 Subject: [PATCH 10/14] Testing timer-self-rescheduling Signed-off-by: Pascal Pieper --- klippy/extras/output_pin.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/klippy/extras/output_pin.py b/klippy/extras/output_pin.py index 9619c82fe2f4..f0ee8fb2f936 100644 --- a/klippy/extras/output_pin.py +++ b/klippy/extras/output_pin.py @@ -39,8 +39,8 @@ def __init__(self, config): if self.host_ack_timeout > 0: self.host_ack_timeout = max(self.host_ack_timeout, 0.500) self.mcu_pin.setup_max_duration(self.host_ack_timeout) - self.resend_timer = self.reactor.register_timer( - self._resend_current_val) + self.resend_timer = None + self.resent_interval = .8 * self.host_ack_timeout - PIN_MIN_TIME self.last_value = config.getfloat( 'value', 0., minval=0., maxval=self.scale) / self.scale @@ -66,14 +66,9 @@ def _set_pin(self, print_time, value, cycle_time, is_resend=False): self.last_value = value self.last_cycle_time = cycle_time self.last_print_time = print_time - if self.host_ack_timeout != 0 and not is_resend: - if value == self.shutdown_value: - self.reactor.update_timer( - self.resend_timer, self.reactor.NEVER) - else: - self.reactor.update_timer( - self.resend_timer, self.reactor.monotonic() - + (0.8 * self.host_ack_timeout) - PIN_MIN_TIME) + if self.host_ack_timeout != 0 and self.resend_timer is None: + self.resend_timer = self.reactor.add_timer( + self._resend_current_val, self.reactor.NOW) cmd_SET_PIN_help = "Set the value of an output pin" def cmd_SET_PIN(self, gcmd): value = gcmd.get_float('VALUE', minval=0., maxval=self.scale) @@ -87,13 +82,20 @@ def cmd_SET_PIN(self, gcmd): lambda print_time: self._set_pin(print_time, value, cycle_time)) def _resend_current_val(self, eventtime): - print_time = self.mcu_pin.get_mcu().estimated_print_time(eventtime) - self._set_pin(print_time + PIN_MIN_TIME, - self.last_value, self.last_cycle_time, True) + if self.last_value == self.shutdown_value: + self.reactor.unregister_timer(self.resend_timer) + self.resend_timer = None + return self.reactor.NEVER - if self.last_value != self.shutdown_value: - return eventtime + (0.8 * self.host_ack_timeout) - PIN_MIN_TIME - return self.reactor.NEVER + systime = self.reactor.monotonic() + print_time = self.mcu_pin.get_mcu().estimated_print_time(systime) + time_diff = print_time - (self.last_print_time + self.resent_interval) + if time_diff > 0.: + # Reschedule for resend time + return systime + time_diff + self._set_pin(print_time + PIN_MIN_TIME, + self.last_value, self.last_cycle_time, True) + return systime + self.resent_interval def load_config_prefix(config): return PrinterOutputPin(config) From 6b8cfb1484040131b42b1e23b7fb46ddb3732bd4 Mon Sep 17 00:00:00 2001 From: Pascal Pieper Date: Wed, 17 Mar 2021 18:24:18 +0100 Subject: [PATCH 11/14] Use actual function --- klippy/extras/output_pin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/extras/output_pin.py b/klippy/extras/output_pin.py index f0ee8fb2f936..b9b1e42f1519 100644 --- a/klippy/extras/output_pin.py +++ b/klippy/extras/output_pin.py @@ -67,7 +67,7 @@ def _set_pin(self, print_time, value, cycle_time, is_resend=False): self.last_cycle_time = cycle_time self.last_print_time = print_time if self.host_ack_timeout != 0 and self.resend_timer is None: - self.resend_timer = self.reactor.add_timer( + self.resend_timer = self.reactor.register_timer( self._resend_current_val, self.reactor.NOW) cmd_SET_PIN_help = "Set the value of an output pin" def cmd_SET_PIN(self, gcmd): From ec893780681c6851948b62ed08e6ebe25c862a8b Mon Sep 17 00:00:00 2001 From: Pascal Pieper Date: Fri, 19 Mar 2021 12:08:31 +0100 Subject: [PATCH 12/14] Updated documentation Signed-off-by: Pascal Pieper --- docs/Using_PWM_Tools.md | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/docs/Using_PWM_Tools.md b/docs/Using_PWM_Tools.md index 46f41ff5d816..8978eae56c36 100644 --- a/docs/Using_PWM_Tools.md +++ b/docs/Using_PWM_Tools.md @@ -5,6 +5,7 @@ using `output_pin` and some macros. ## How does it work? With re-purposing the printhead's fan pwm output, you can control lasers or spindles. +This is useful if you use switchable print heads, for example. Usually, cam-tools such as LaserWeb can be configured to use `M3-M5` commands, which stand for _spindle speed CW_ (`M3 S[0-255]`), _spindle speed CCW_ (`M4 S[0-255]`) and _spindle stop_ (`M5`). @@ -13,11 +14,11 @@ For safety reasons, you should configure a safety timeout, so that when your host or MCU encounters an error, the tool will stop. -## Configuration +## Example Configuration [output_pin TOOL] - pin: !ar9 + pin: !ar9 # use your fan's pin number pwm: True - hardware_pwm: False + hardware_pwm: True cycle_time: 0.001 shutdown_value: 0 @@ -33,16 +34,38 @@ so that when your host or MCU encounters an error, the tool will stop. [gcode_macro M3] default_parameter_S: 0 gcode: - SET_PIN PIN=TOOL VALUE={S|float / 255} + SET_PIN PIN=TOOL VALUE={S|float / 255} [gcode_macro M4] default_parameter_S: 0 gcode: - SET_PIN PIN=TOOL VALUE={S|float / 255} + SET_PIN PIN=TOOL VALUE={S|float / 255} [gcode_macro M5] gcode: - SET_PIN PIN=TOOL VALUE=0 + SET_PIN PIN=TOOL VALUE=0 + + [menu __main __control __toolonoff] + type: input + enable: {'output_pin TOOL' in printer} + name: Fan: {'ON ' if menu.input else 'OFF'} + input: {printer['output_pin TOOL'].value} + input_min: 0 + input_max: 1 + input_step: 1 + gcode: + M3 S{255 if menu.input else 0} + + [menu __main __control __toolspeed] + type: input + enable: {'output_pin TOOL' in printer} + name: Tool speed: {'%3d' % (menu.input*100)}% + input: {printer['output_pin TOOL'].value} + input_min: 0 + input_max: 1 + input_step: 0.01 + gcode: + M3 S{'%d' % (menu.input*255)} ## Commands From 84b26298cf2eeb73c41f03b61138b7c67e260fc1 Mon Sep 17 00:00:00 2001 From: Pascal Pieper Date: Fri, 26 Mar 2021 10:06:31 +0100 Subject: [PATCH 13/14] Code cleanup and rename of host_acknowledge_timeout to maximum_mcu_duration Signed-off-by: Pascal Pieper --- docs/Config_Reference.md | 2 +- docs/Using_PWM_Tools.md | 2 +- klippy/extras/output_pin.py | 23 ++++++++++------------- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index f4a67071a181..bb60d823cbd7 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -2484,7 +2484,7 @@ pin: #shutdown_value: # The value to set the pin to on an MCU shutdown event. The default # is 0 (for low voltage). -#host_acknowledge_timeout: +#maximum_mcu_duration: # The maximum duration a non-shutdown value may be driven by the MCU # without an acknowledge from the host. # If host can not keep up with an update, the MCU will shutdown diff --git a/docs/Using_PWM_Tools.md b/docs/Using_PWM_Tools.md index 8978eae56c36..0c1919aba1b4 100644 --- a/docs/Using_PWM_Tools.md +++ b/docs/Using_PWM_Tools.md @@ -22,7 +22,7 @@ so that when your host or MCU encounters an error, the tool will stop. cycle_time: 0.001 shutdown_value: 0 - host_acknowledge_timeout: 5 + maximum_mcu_duration: 5 # Default: 0 (disabled) # Amount of time in which the host has to acknowledge # a non-shutdown output value. diff --git a/klippy/extras/output_pin.py b/klippy/extras/output_pin.py index b9b1e42f1519..d2b1a079bb44 100644 --- a/klippy/extras/output_pin.py +++ b/klippy/extras/output_pin.py @@ -6,7 +6,6 @@ PIN_MIN_TIME = 0.100 - class PrinterOutputPin: def __init__(self, config): self.printer = config.get_printer() @@ -26,21 +25,19 @@ def __init__(self, config): self.last_print_time = 0. static_value = config.getfloat('static_value', None, minval=0., maxval=self.scale) + self.reactor = self.printer.get_reactor() + self.resend_timer = None + self.resend_interval = 0 if static_value is not None: self.mcu_pin.setup_max_duration(0.) self.last_value = static_value / self.scale self.mcu_pin.setup_start_value( self.last_value, self.last_value, True) else: - self.reactor = self.printer.get_reactor() - self.host_ack_timeout = config.getfloat('host_acknowledge_timeout', - 0, minval=0.) - #ensure that safety timeout is big enough for comm. latency - if self.host_ack_timeout > 0: - self.host_ack_timeout = max(self.host_ack_timeout, 0.500) - self.mcu_pin.setup_max_duration(self.host_ack_timeout) - self.resend_timer = None - self.resent_interval = .8 * self.host_ack_timeout - PIN_MIN_TIME + self.max_mcu_duration = config.getfloat('maximum_mcu_duration', + 0, minval=0.500) + self.mcu_pin.setup_max_duration(self.max_mcu_duration) + self.resend_interval = .8 * self.max_mcu_duration - PIN_MIN_TIME self.last_value = config.getfloat( 'value', 0., minval=0., maxval=self.scale) / self.scale @@ -66,7 +63,7 @@ def _set_pin(self, print_time, value, cycle_time, is_resend=False): self.last_value = value self.last_cycle_time = cycle_time self.last_print_time = print_time - if self.host_ack_timeout != 0 and self.resend_timer is None: + if self.max_mcu_duration != 0 and self.resend_timer is None: self.resend_timer = self.reactor.register_timer( self._resend_current_val, self.reactor.NOW) cmd_SET_PIN_help = "Set the value of an output pin" @@ -89,13 +86,13 @@ def _resend_current_val(self, eventtime): systime = self.reactor.monotonic() print_time = self.mcu_pin.get_mcu().estimated_print_time(systime) - time_diff = print_time - (self.last_print_time + self.resent_interval) + time_diff = print_time - (self.last_print_time + self.resend_interval) if time_diff > 0.: # Reschedule for resend time return systime + time_diff self._set_pin(print_time + PIN_MIN_TIME, self.last_value, self.last_cycle_time, True) - return systime + self.resent_interval + return systime + self.resend_interval def load_config_prefix(config): return PrinterOutputPin(config) From d5892db69a4008afe4912b189894c16f73b376d1 Mon Sep 17 00:00:00 2001 From: Pascal Pieper Date: Fri, 26 Mar 2021 11:00:46 +0100 Subject: [PATCH 14/14] Added more documentation Signed-off-by: Pascal Pieper --- docs/Overview.md | 2 + docs/Using_PWM_Tools.md | 104 +++++++++++++++++++--------------------- 2 files changed, 51 insertions(+), 55 deletions(-) diff --git a/docs/Overview.md b/docs/Overview.md index 74ad12261b6b..5036f3a01aba 100644 --- a/docs/Overview.md +++ b/docs/Overview.md @@ -44,6 +44,8 @@ communication with the Klipper developers. sensorless homing. - [Skew correction](skew_correction.md): Adjustments for axes not perfectly square. +- [PWM tools](Using_PWM_Tools.md): Guide on how to use PWM controlled + tools such as lasers or spindles. - [G-Codes](G-Codes.md): Information on commands supported by Klipper. # Developer Documentation diff --git a/docs/Using_PWM_Tools.md b/docs/Using_PWM_Tools.md index 0c1919aba1b4..3947e183d4b1 100644 --- a/docs/Using_PWM_Tools.md +++ b/docs/Using_PWM_Tools.md @@ -1,73 +1,67 @@ -This document describes how to setup a PWM-Controlled laser or spindle +This document describes how to setup a PWM-controlled laser or spindle using `output_pin` and some macros. ## How does it work? With re-purposing the printhead's fan pwm output, you can control lasers or spindles. -This is useful if you use switchable print heads, for example. +This is useful if you use switchable print heads, for example +the E3D toolchanger or a DIY solution. Usually, cam-tools such as LaserWeb can be configured to use `M3-M5` commands, which stand for _spindle speed CW_ (`M3 S[0-255]`), _spindle speed CCW_ (`M4 S[0-255]`) and _spindle stop_ (`M5`). -For safety reasons, you should configure a safety timeout, + +**Warning:** When driving a laser, keep all security precautions +that you can think of! Diode lasers are usually inverted. +This means, that when the MCU restarts, the laser will be +_fully on_ for the time it takes the MCU to start up again. +For good measure, it is recommended to _always_ wear appropriate +laser-goggles of the right wavelength if the laser is powered; +and to disconnect the laser when it is not needed. +Also, you should configure a safety timeout, so that when your host or MCU encounters an error, the tool will stop. +For an example configuration, see `config/sample-pwm-tool-cfg`. + +## Current Limitations -## Example Configuration - [output_pin TOOL] - pin: !ar9 # use your fan's pin number - pwm: True - hardware_pwm: True - cycle_time: 0.001 - shutdown_value: 0 - - maximum_mcu_duration: 5 - # Default: 0 (disabled) - # Amount of time in which the host has to acknowledge - # a non-shutdown output value. - # Suggested value is around 5 seconds. - # Use a value that does not burn up your stock. - # Please note that during homing, your tool - # needs to be in default speed. - - [gcode_macro M3] - default_parameter_S: 0 - gcode: - SET_PIN PIN=TOOL VALUE={S|float / 255} - - [gcode_macro M4] - default_parameter_S: 0 - gcode: - SET_PIN PIN=TOOL VALUE={S|float / 255} - - [gcode_macro M5] - gcode: - SET_PIN PIN=TOOL VALUE=0 - - [menu __main __control __toolonoff] - type: input - enable: {'output_pin TOOL' in printer} - name: Fan: {'ON ' if menu.input else 'OFF'} - input: {printer['output_pin TOOL'].value} - input_min: 0 - input_max: 1 - input_step: 1 - gcode: - M3 S{255 if menu.input else 0} - - [menu __main __control __toolspeed] - type: input - enable: {'output_pin TOOL' in printer} - name: Tool speed: {'%3d' % (menu.input*100)}% - input: {printer['output_pin TOOL'].value} - input_min: 0 - input_max: 1 - input_step: 0.01 - gcode: - M3 S{'%d' % (menu.input*255)} +There is a limitation of how frequent PWM updates may occur. +While being very precise, a PWM update may only occur every 0.1 seconds, +rendering it almost useless for raster engraving. +However, there exists an [experimental branch](https://github.com/Cirromulus/klipper/tree/laser_tool) with its own tradeoffs. +In long term, it is planned to add this functionality to main-line klipper. ## Commands `M3/M4 S` : Set PWM duty-cycle. Values between 0 and 255. `M5` : Stop PWM output to shutdown value. + +## Laserweb Configuration + +If you use Laserweb, a working configuration would be: + + GCODE START: + M5 ; Disable Laser + G21 ; Set units to mm + G90 ; Absolute positioning + G0 Z0 F7000 ; Set Non-Cutting speed + + GCODE END: + M5 ; Disable Laser + G91 ; relative + G0 Z+20 F4000 ; + G90 ; absolute + + GCODE HOMING: + M5 ; Disable Laser + G28 ; Home all axis + + TOOL ON: + M3 $INTENSITY + + TOOL OFF: + M5 ; Disable Laser + + LASER INTENSITY: + S