diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 1c86acf5..cca85161 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -2529,6 +2529,13 @@ pin: #shutdown_value: # The value to set the pin to on an MCU shutdown event. The default # is 0 (for low voltage). +#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 +# 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/Overview.md b/docs/Overview.md index 94ffd8fa..a82f372b 100644 --- a/docs/Overview.md +++ b/docs/Overview.md @@ -46,6 +46,8 @@ communication with the Klipper developers. with Klipper. - [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 new file mode 100644 index 00000000..3947e183 --- /dev/null +++ b/docs/Using_PWM_Tools.md @@ -0,0 +1,67 @@ +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 +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`). + + +**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 + +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 diff --git a/klippy/extras/output_pin.py b/klippy/extras/output_pin.py index f9a886f6..d2b1a079 100644 --- a/klippy/extras/output_pin.py +++ b/klippy/extras/output_pin.py @@ -22,20 +22,28 @@ class PrinterOutputPin: 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) + 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.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 - 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 +51,10 @@ class PrinterOutputPin: 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 +63,9 @@ class PrinterOutputPin: self.last_value = value self.last_cycle_time = cycle_time self.last_print_time = print_time + 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" def cmd_SET_PIN(self, gcmd): value = gcmd.get_float('VALUE', minval=0., maxval=self.scale) @@ -66,5 +78,21 @@ class PrinterOutputPin: toolhead.register_lookahead_callback( lambda print_time: self._set_pin(print_time, value, cycle_time)) + def _resend_current_val(self, eventtime): + if self.last_value == self.shutdown_value: + self.reactor.unregister_timer(self.resend_timer) + self.resend_timer = None + 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.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.resend_interval + def load_config_prefix(config): return PrinterOutputPin(config)