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 'tool' module #3995

Merged
merged 14 commits into from
Mar 26, 2021
Merged
Show file tree
Hide file tree
Changes from 3 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
7 changes: 7 additions & 0 deletions docs/Config_Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
48 changes: 48 additions & 0 deletions docs/Using_PWM_Tools.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
This document describes how to setup a PWM-Controlled laser or spindle
using `output_pin` and some macros.
Cirromulus marked this conversation as resolved.
Show resolved Hide resolved


## 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<value>` : Set PWM duty-cycle. Values between 0 and 255.
`M5` : Stop PWM output to shutdown value.
35 changes: 30 additions & 5 deletions klippy/extras/output_pin.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,30 +22,38 @@ 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()
Cirromulus marked this conversation as resolved.
Show resolved Hide resolved
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(
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,
self.cmd_SET_PIN,
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)
Expand All @@ -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)
Expand All @@ -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)
)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks. Unfortunately, it's not valid to call toolhead.register_lookahead_callback() without holding the gcode mutex. That is, it can't be called directly from a reactor callback. You probably want to do something like print_time = self.mcu_pwm.get_mcu().estimated_print_time(eventtime) instead - though the time will need to be padded by 100ms (to allow for transmission time to the mcu).

-Kevin

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is it allowed to just set the pin directly, as it is now?
100ms time padding is now explicitly in the "relax_margin" which is supposed to give the scheduler some time.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The toolhead.get_last_move_time() and related toolhead.register_lookahead_callback() always return a time in the future, so that it is possible to schedule events at that time. A call to mcu.estimated_print_time(systime) returns the time of "now" on the micro-controller - if one were to schedule an event at that time, it would always result in a "timer too close" as it takes time to transmit that schedule to the micro-controller.

I'm not sure what the goal of the "relax_margin" is. You only need the PIN_MIN_TIME - any additional time is probably not a good thing as it limits how response future updates are.

-Kevin

Copy link
Contributor Author

@Cirromulus Cirromulus Mar 15, 2021

Choose a reason for hiding this comment

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

the "relax margin" was supposed to reduce stress between "now" and the scheduled time the pin has to update (reducing the possibility of too close timers).
But if you say that this just removes responsiveness, it can be replaced for just the PIN_MIN_TIME, instead of 10% timeout + PIN_MIN_TIME.

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)