├── .gitignore ├── README.md ├── fan_extra.py ├── heater_fan_extra.py ├── heaters_extra.py ├── pid_calibrate_extra.py └── rotation_distance.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Extras 2 | 3 | ## Usage 4 | 5 | Copy `.py` files to `klipper/klippy/extras` 6 | 7 | ### pid_calibrate_extra and heaters_extra 8 | 9 | Add to beginning of `printer.cfg` 10 | 11 | ``` 12 | [heaters_extra] 13 | [pid_calibrate_extra] 14 | ``` 15 | 16 | ### fan_extra 17 | 18 | Use `[fan_extra]` instead of `[fan]` 19 | 20 | ### rotation_distance 21 | 22 | Add to beginning of `printer.cfg` 23 | 24 | ``` 25 | [rotation_distance] 26 | ``` 27 | 28 | ### Applying 29 | 30 | Restart klipper service, RESTART command won't activate modules! 31 | 32 | ## Changes 33 | 34 | ### heater_fan_extra 35 | 36 | Use `[heater_fan_extra extruder]` instead of `[heater_fan extruder]` 37 | 38 | gcode commands: 39 | 40 | `SET_HEATER_FAN_SPEED FAN=extruder SPEED=[0 - 1.0]` 41 | 42 | ### heaters_extra 43 | 44 | `heater` option: 45 | 46 | `control: pid_v` 47 | 48 | Activates Velocity PID 49 | 50 | gcode commands: 51 | 52 | `SET_HEATER_PARAMS HEATER= KP= KI= KD=` 53 | 54 | for pid controlled heater 55 | 56 | `SET_HEATER_PARAMS HEATER= MAX_DELTA=` 57 | 58 | for bang-bang heater 59 | 60 | `GET_HEATER_PARAMS HEATER=` 61 | 62 | Prints current heater params 63 | 64 | ### pid_calibrate_extra 65 | 66 | gcode command 67 | 68 | `PID_CALIBRATE_EXTRA` 69 | 70 | same options as default `PID_CALIBRATE` 71 | 72 | ### fan_extra 73 | 74 | Scaling fan value between `off_below` and `max_power`. For example with followong config: 75 | 76 | ``` 77 | [fan_extra] 78 | off_below: 0.2 79 | max_power: 0.8 80 | ... 81 | ``` 82 | 83 | Calling `M106 S1` will set output to 0.2, `M106 S255` will set output to 0.8, and all values between would be linear scaled accordingly. 84 | 85 | ### rotation_distance 86 | 87 | Calculate and apply new rotation distance based on extrude calibration 88 | 89 | `ROTATION_DISTANCE_CALC EXTRUDER=extruder EXTRUDED=101 REQUESTED=100` 90 | 91 | Save new rotation_distance value to config file 92 | 93 | `ROTATION_DISTANCE_SAVE EXTRUDER=extruder` 94 | 95 | 96 | ## Credits 97 | 98 | ### https://github.com/Laker87/klipper: 99 | 100 | heaters_extra 101 | 102 | pid_calibrate_extra 103 | -------------------------------------------------------------------------------- /fan_extra.py: -------------------------------------------------------------------------------- 1 | # Printer cooling fan 2 | # 3 | # Copyright (C) 2016-2020 Kevin O'Connor 4 | # 5 | # This file may be distributed under the terms of the GNU GPLv3 license. 6 | from . import pulse_counter 7 | 8 | MIN_PWM = 1. / 255. 9 | 10 | class Fan: 11 | def __init__(self, config, default_shutdown_speed=0.): 12 | self.printer = config.get_printer() 13 | self.printer.add_object('fan', self) 14 | self.last_fan_value = 0. 15 | self.last_fan_time = 0. 16 | # Read config 17 | self.max_power = config.getfloat('max_power', 1., above=0., maxval=1.) 18 | self.kick_start_time = config.getfloat('kick_start_time', 0.1, 19 | minval=0.) 20 | self.off_below = config.getfloat('off_below', default=0., 21 | minval=0., maxval=1.) 22 | cycle_time = config.getfloat('cycle_time', 0.010, above=0.) 23 | self.min_time = config.getfloat('min_time', 0.1, minval=0.) 24 | hardware_pwm = config.getboolean('hardware_pwm', False) 25 | shutdown_speed = config.getfloat( 26 | 'shutdown_speed', default_shutdown_speed, minval=0., maxval=1.) 27 | # Setup pwm object 28 | ppins = self.printer.lookup_object('pins') 29 | self.mcu_fan = ppins.setup_pin('pwm', config.get('pin')) 30 | self.mcu_fan.setup_max_duration(0.) 31 | self.mcu_fan.setup_cycle_time(cycle_time, hardware_pwm) 32 | shutdown_power = max(0., min(self.max_power, shutdown_speed)) 33 | self.mcu_fan.setup_start_value(0., shutdown_power) 34 | 35 | self.enable_pin = None 36 | enable_pin = config.get('enable_pin', None) 37 | if enable_pin is not None: 38 | self.enable_pin = ppins.setup_pin('digital_out', enable_pin) 39 | self.enable_pin.setup_max_duration(0.) 40 | 41 | # Setup tachometer 42 | self.tachometer = FanTachometer(config) 43 | 44 | # Register callbacks 45 | self.printer.register_event_handler("gcode:request_restart", 46 | self._handle_request_restart) 47 | 48 | def get_mcu(self): 49 | return self.mcu_fan.get_mcu() 50 | def set_speed(self, print_time, init_value): 51 | value = init_value 52 | if value > 0: 53 | value = (((value - MIN_PWM) * (self.max_power - self.off_below)) / (1 - MIN_PWM)) + self.off_below 54 | if value == self.last_fan_value: 55 | return 56 | print_time = max(self.last_fan_time + self.min_time, print_time) 57 | if self.enable_pin: 58 | if value > 0 and self.last_fan_value == 0: 59 | self.enable_pin.set_digital(print_time, 1) 60 | elif value == 0 and self.last_fan_value > 0: 61 | self.enable_pin.set_digital(print_time, 0) 62 | if (value and value < self.max_power and self.kick_start_time 63 | and (not self.last_fan_value or value - self.last_fan_value > .5)): 64 | # Run fan at full speed for specified kick_start_time 65 | self.mcu_fan.set_pwm(print_time, self.max_power) 66 | print_time += self.kick_start_time 67 | self.mcu_fan.set_pwm(print_time, value) 68 | self.last_fan_time = print_time 69 | self.last_fan_value = init_value 70 | def set_speed_from_command(self, value): 71 | toolhead = self.printer.lookup_object('toolhead') 72 | toolhead.register_lookahead_callback((lambda pt: 73 | self.set_speed(pt, value))) 74 | def _handle_request_restart(self, print_time): 75 | self.set_speed(print_time, 0.) 76 | 77 | def get_status(self, eventtime): 78 | tachometer_status = self.tachometer.get_status(eventtime) 79 | return { 80 | 'speed': self.last_fan_value, 81 | 'rpm': tachometer_status['rpm'], 82 | } 83 | 84 | class FanTachometer: 85 | def __init__(self, config): 86 | printer = config.get_printer() 87 | self._freq_counter = None 88 | 89 | pin = config.get('tachometer_pin', None) 90 | if pin is not None: 91 | self.ppr = config.getint('tachometer_ppr', 2, minval=1) 92 | poll_time = config.getfloat('tachometer_poll_interval', 93 | 0.0015, above=0.) 94 | sample_time = 1. 95 | self._freq_counter = pulse_counter.FrequencyCounter( 96 | printer, pin, sample_time, poll_time) 97 | 98 | def get_status(self, eventtime): 99 | if self._freq_counter is not None: 100 | rpm = self._freq_counter.get_frequency() * 30. / self.ppr 101 | else: 102 | rpm = None 103 | return {'rpm': rpm} 104 | 105 | class PrinterFanExtra: 106 | def __init__(self, config): 107 | self.fan = Fan(config) 108 | # Register commands 109 | gcode = config.get_printer().lookup_object('gcode') 110 | gcode.register_command("M106", self.cmd_M106) 111 | gcode.register_command("M107", self.cmd_M107) 112 | def get_status(self, eventtime): 113 | return self.fan.get_status(eventtime) 114 | def cmd_M106(self, gcmd): 115 | # Set fan speed 116 | value = gcmd.get_float('S', 255., minval=0.) / 255. 117 | self.fan.set_speed_from_command(value) 118 | def cmd_M107(self, gcmd): 119 | # Turn fan off 120 | self.fan.set_speed_from_command(0.) 121 | 122 | def load_config(config): 123 | return PrinterFanExtra(config) 124 | -------------------------------------------------------------------------------- /heater_fan_extra.py: -------------------------------------------------------------------------------- 1 | # Support fans that are enabled when a heater is on 2 | # 3 | # Copyright (C) 2016-2020 Kevin O'Connor 4 | # 5 | # This file may be distributed under the terms of the GNU GPLv3 license. 6 | from . import fan 7 | 8 | PIN_MIN_TIME = 0.100 9 | 10 | class PrinterHeaterFan: 11 | def __init__(self, config): 12 | self.name = config.get_name().split()[-1] 13 | self.printer = config.get_printer() 14 | self.printer.load_object(config, 'heaters') 15 | self.printer.register_event_handler("klippy:ready", self.handle_ready) 16 | self.heater_names = config.getlist("heater", ("extruder",)) 17 | self.heater_temp = config.getfloat("heater_temp", 50.0) 18 | self.heaters = [] 19 | self.fan = fan.Fan(config, default_shutdown_speed=1.) 20 | self.fan_speed = config.getfloat("fan_speed", 1., minval=0., maxval=1.) 21 | self.last_speed = 0. 22 | gcode = self.printer.lookup_object("gcode") 23 | gcode.register_mux_command("SET_HEATER_FAN_SPEED", "FAN", 24 | self.name, self.cmd_SET_HEATER_FAN_SPEED, 25 | desc=self.cmd_SET_HEATER_FAN_SPEED_help) 26 | 27 | cmd_SET_HEATER_FAN_SPEED_help = "Sets heater fan target speed" 28 | def cmd_SET_HEATER_FAN_SPEED(self, gcmd): 29 | self.fan_speed = gcmd.get_float('SPEED', self.fan_speed) 30 | 31 | def handle_ready(self): 32 | pheaters = self.printer.lookup_object('heaters') 33 | self.heaters = [pheaters.lookup_heater(n) for n in self.heater_names] 34 | reactor = self.printer.get_reactor() 35 | reactor.register_timer(self.callback, reactor.monotonic()+PIN_MIN_TIME) 36 | def get_status(self, eventtime): 37 | return self.fan.get_status(eventtime) 38 | def callback(self, eventtime): 39 | speed = 0. 40 | for heater in self.heaters: 41 | current_temp, target_temp = heater.get_temp(eventtime) 42 | if target_temp or current_temp > self.heater_temp: 43 | speed = self.fan_speed 44 | if speed != self.last_speed: 45 | self.last_speed = speed 46 | curtime = self.printer.get_reactor().monotonic() 47 | print_time = self.fan.get_mcu().estimated_print_time(curtime) 48 | self.fan.set_speed(print_time + PIN_MIN_TIME, speed) 49 | return eventtime + 1. 50 | 51 | def load_config_prefix(config): 52 | return PrinterHeaterFan(config) 53 | -------------------------------------------------------------------------------- /heaters_extra.py: -------------------------------------------------------------------------------- 1 | # Tracking of PWM controlled heaters and their temperature control 2 | # 3 | # Copyright (C) 2016-2020 Kevin O'Connor 4 | # 5 | # This file may be distributed under the terms of the GNU GPLv3 license. 6 | import os, logging, threading 7 | 8 | 9 | ###################################################################### 10 | # Heater 11 | ###################################################################### 12 | 13 | KELVIN_TO_CELSIUS = -273.15 14 | MAX_HEAT_TIME = 5.0 15 | AMBIENT_TEMP = 25. 16 | PID_PARAM_BASE = 255. 17 | 18 | class Heater: 19 | def __init__(self, config, sensor): 20 | self.printer = config.get_printer() 21 | self.name = config.get_name().split()[-1] 22 | # Setup sensor 23 | self.sensor = sensor 24 | self.min_temp = config.getfloat('min_temp', minval=KELVIN_TO_CELSIUS) 25 | self.max_temp = config.getfloat('max_temp', above=self.min_temp) 26 | self.sensor.setup_minmax(self.min_temp, self.max_temp) 27 | self.sensor.setup_callback(self.temperature_callback) 28 | self.pwm_delay = self.sensor.get_report_time_delta() 29 | # Setup temperature checks 30 | self.min_extrude_temp = config.getfloat( 31 | 'min_extrude_temp', 170., 32 | minval=self.min_temp, maxval=self.max_temp) 33 | is_fileoutput = (self.printer.get_start_args().get('debugoutput') 34 | is not None) 35 | self.can_extrude = self.min_extrude_temp <= 0. or is_fileoutput 36 | self.max_power = config.getfloat('max_power', 1., above=0., maxval=1.) 37 | self.smooth_time = config.getfloat('smooth_time', 1., above=0.) 38 | self.inv_smooth_time = 1. / self.smooth_time 39 | self.lock = threading.Lock() 40 | self.last_temp = self.smoothed_temp = self.target_temp = 0. 41 | self.last_temp_time = 0. 42 | # pwm caching 43 | self.next_pwm_time = 0. 44 | self.last_pwm_value = 0. 45 | # Setup control algorithm sub-class 46 | algos = { 47 | 'watermark': ControlBangBang, 48 | 'pid': ControlPID, 49 | 'pid_v': ControlVelocityPID, 50 | } 51 | algo = config.getchoice('control', algos) 52 | self.control = algo(self, config) 53 | # Setup output heater pin 54 | heater_pin = config.get('heater_pin') 55 | ppins = self.printer.lookup_object('pins') 56 | self.mcu_pwm = ppins.setup_pin('pwm', heater_pin) 57 | pwm_cycle_time = config.getfloat('pwm_cycle_time', 0.100, above=0., 58 | maxval=self.pwm_delay) 59 | self.mcu_pwm.setup_cycle_time(pwm_cycle_time) 60 | self.mcu_pwm.setup_max_duration(MAX_HEAT_TIME) 61 | # Load additional modules 62 | self.printer.load_object(config, "verify_heater %s" % (self.name,)) 63 | self.printer.load_object(config, "pid_calibrate") 64 | gcode = self.printer.lookup_object("gcode") 65 | gcode.register_mux_command("SET_HEATER_TEMPERATURE", "HEATER", 66 | self.name, self.cmd_SET_HEATER_TEMPERATURE, 67 | desc=self.cmd_SET_HEATER_TEMPERATURE_help) 68 | gcode.register_mux_command("GET_HEATER_PARAMS", "HEATER", 69 | self.name, self.cmd_GET_HEATER_PARAMS, 70 | desc=self.cmd_GET_HEATER_PARAMS_help) 71 | gcode.register_mux_command("SET_HEATER_PARAMS", "HEATER", 72 | self.name, self.cmd_SET_HEATER_PARAMS, 73 | desc=self.cmd_SET_HEATER_PARAMS_help) 74 | def set_pwm(self, read_time, value): 75 | if self.target_temp <= 0.: 76 | value = 0. 77 | if ((read_time < self.next_pwm_time or not self.last_pwm_value) 78 | and abs(value - self.last_pwm_value) < 0.05): 79 | # No significant change in value - can suppress update 80 | return 81 | pwm_time = read_time + self.pwm_delay 82 | self.next_pwm_time = pwm_time + 0.75 * MAX_HEAT_TIME 83 | self.last_pwm_value = value 84 | self.mcu_pwm.set_pwm(pwm_time, value) 85 | #logging.debug("%s: pwm=%.3f@%.3f (from %.3f@%.3f [%.3f])", 86 | # self.name, value, pwm_time, 87 | # self.last_temp, self.last_temp_time, self.target_temp) 88 | def temperature_callback(self, read_time, temp): 89 | with self.lock: 90 | time_diff = read_time - self.last_temp_time 91 | self.last_temp = temp 92 | self.last_temp_time = read_time 93 | self.control.temperature_update(read_time, temp, self.target_temp) 94 | temp_diff = temp - self.smoothed_temp 95 | adj_time = min(time_diff * self.inv_smooth_time, 1.) 96 | self.smoothed_temp += temp_diff * adj_time 97 | self.can_extrude = (self.smoothed_temp >= self.min_extrude_temp) 98 | #logging.debug("temp: %.3f %f = %f", read_time, temp) 99 | # External commands 100 | def get_pwm_delay(self): 101 | return self.pwm_delay 102 | def get_max_power(self): 103 | return self.max_power 104 | def get_smooth_time(self): 105 | return self.smooth_time 106 | def set_temp(self, degrees): 107 | if degrees and (degrees < self.min_temp or degrees > self.max_temp): 108 | raise self.printer.command_error( 109 | "Requested temperature (%.1f) out of range (%.1f:%.1f)" 110 | % (degrees, self.min_temp, self.max_temp)) 111 | with self.lock: 112 | self.target_temp = degrees 113 | def get_temp(self, eventtime): 114 | print_time = self.mcu_pwm.get_mcu().estimated_print_time(eventtime) - 5. 115 | with self.lock: 116 | if self.last_temp_time < print_time: 117 | return 0., self.target_temp 118 | return self.smoothed_temp, self.target_temp 119 | def check_busy(self, eventtime): 120 | with self.lock: 121 | return self.control.check_busy( 122 | eventtime, self.smoothed_temp, self.target_temp) 123 | def set_control(self, control): 124 | with self.lock: 125 | old_control = self.control 126 | self.control = control 127 | self.target_temp = 0. 128 | return old_control 129 | def alter_target(self, target_temp): 130 | if target_temp: 131 | target_temp = max(self.min_temp, min(self.max_temp, target_temp)) 132 | self.target_temp = target_temp 133 | def stats(self, eventtime): 134 | with self.lock: 135 | target_temp = self.target_temp 136 | last_temp = self.last_temp 137 | last_pwm_value = self.last_pwm_value 138 | is_active = target_temp or last_temp > 50. 139 | return is_active, '%s: target=%.0f temp=%.1f pwm=%.3f' % ( 140 | self.name, target_temp, last_temp, last_pwm_value) 141 | def get_status(self, eventtime): 142 | with self.lock: 143 | target_temp = self.target_temp 144 | smoothed_temp = self.smoothed_temp 145 | last_pwm_value = self.last_pwm_value 146 | return {'temperature': round(smoothed_temp, 2), 'target': target_temp, 147 | 'power': last_pwm_value} 148 | cmd_SET_HEATER_TEMPERATURE_help = "Sets a heater temperature" 149 | def cmd_SET_HEATER_TEMPERATURE(self, gcmd): 150 | temp = gcmd.get_float('TARGET', 0.) 151 | pheaters = self.printer.lookup_object('heaters') 152 | pheaters.set_temperature(self, temp) 153 | cmd_GET_HEATER_PARAMS_help = "Gets params from heater" 154 | def cmd_GET_HEATER_PARAMS(self, gcmd): 155 | self.control.get_params(gcmd) 156 | cmd_SET_HEATER_PARAMS_help = "Sets params for heater" 157 | def cmd_SET_HEATER_PARAMS(self, gcmd): 158 | self.control.set_params(gcmd) 159 | 160 | 161 | ###################################################################### 162 | # Bang-bang control algo 163 | ###################################################################### 164 | 165 | class ControlBangBang: 166 | def __init__(self, heater, config): 167 | self.heater = heater 168 | self.heater_max_power = heater.get_max_power() 169 | self.max_delta = config.getfloat('max_delta', 2.0, above=0.) 170 | self.heating = False 171 | def temperature_update(self, read_time, temp, target_temp): 172 | if self.heating and temp >= target_temp+self.max_delta: 173 | self.heating = False 174 | elif not self.heating and temp <= target_temp-self.max_delta: 175 | self.heating = True 176 | if self.heating: 177 | self.heater.set_pwm(read_time, self.heater_max_power) 178 | else: 179 | self.heater.set_pwm(read_time, 0.) 180 | def check_busy(self, eventtime, smoothed_temp, target_temp): 181 | return smoothed_temp < target_temp-self.max_delta 182 | def get_params(self, gcmd): 183 | gcmd.respond_info( 184 | "PID parameters: max_delta=%.3f" % (self.max_delta,)) 185 | def set_params(self, gcmd): 186 | self.max_delta = gcmd.get_float('MAX_DELTA', self.max_delta) 187 | self.get_params(gcmd) 188 | 189 | 190 | ###################################################################### 191 | # Proportional Integral Derivative (PID) control algo 192 | ###################################################################### 193 | 194 | PID_SETTLE_DELTA = 1. 195 | PID_SETTLE_SLOPE = .1 196 | 197 | class ControlPID: 198 | def __init__(self, heater, config): 199 | self.heater = heater 200 | self.heater_max_power = heater.get_max_power() 201 | self.Kp = config.getfloat('pid_Kp') / PID_PARAM_BASE 202 | self.Ki = config.getfloat('pid_Ki') / PID_PARAM_BASE 203 | self.Kd = config.getfloat('pid_Kd') / PID_PARAM_BASE 204 | self.dt = heater.pwm_delay 205 | self.smooth = 1. + heater.get_smooth_time() / self.dt 206 | self.prev_temp = AMBIENT_TEMP 207 | self.prev_err = 0. 208 | self.prev_der = 0. 209 | self.int_sum = 0. 210 | 211 | def temperature_update(self, read_time, temp, target_temp): 212 | # calculate the error 213 | err = target_temp - temp 214 | # calculate the current integral amount using the Trapezoidal rule 215 | ic = ((self.prev_err + err) / 2.) * self.dt 216 | i = self.int_sum + ic 217 | # calculate the current derivative using a modified moving average, 218 | # and derivative on measurement, to account for derivative kick 219 | # when the set point changes 220 | dc = -(temp - self.prev_temp) / self.dt 221 | dc = ((self.smooth - 1.) * self.prev_der + dc)/self.smooth 222 | # calculate the output 223 | o = self.Kp * err + self.Ki * i + self.Kd * dc 224 | # calculate the saturated output 225 | so = max(0., min(self.heater_max_power, o)) 226 | 227 | # update the heater 228 | self.heater.set_pwm(read_time, so) 229 | #update the previous values 230 | self.prev_temp = temp 231 | self.prev_der = dc 232 | if target_temp > 0.: 233 | self.prev_err = err 234 | if o == so: 235 | # not saturated so an update is allowed 236 | self.int_sum = i 237 | else: 238 | # saturated, so conditionally integrate 239 | if (o>0.)-(o<0.) != (ic>0.)-(ic<0.): 240 | # the signs are opposite so an update is allowed 241 | self.int_sum = i 242 | else: 243 | self.prev_err = 0. 244 | self.int_sum = 0. 245 | 246 | def check_busy(self, eventtime, smoothed_temp, target_temp): 247 | temp_diff = target_temp - smoothed_temp 248 | return (abs(temp_diff) > PID_SETTLE_DELTA 249 | or abs(self.prev_der) > PID_SETTLE_SLOPE) 250 | 251 | def get_params(self, gcmd): 252 | gcmd.respond_info( 253 | "PID parameters: pid_Kp=%.3f pid_Ki=%.3f pid_Kd=%.3f" 254 | % (self.Kp * PID_PARAM_BASE, 255 | self.Ki * PID_PARAM_BASE, 256 | self.Kd * PID_PARAM_BASE)) 257 | 258 | def set_params(self, gcmd): 259 | self.Kp = gcmd.get_float('KP', self.Kp * PID_PARAM_BASE) / PID_PARAM_BASE 260 | self.Ki = gcmd.get_float('KI', self.Ki * PID_PARAM_BASE) / PID_PARAM_BASE 261 | self.Kd = gcmd.get_float('KD', self.Kd * PID_PARAM_BASE) / PID_PARAM_BASE 262 | self.get_params(gcmd) 263 | 264 | 265 | ###################################################################### 266 | # Velocity (PID) control algo 267 | ###################################################################### 268 | 269 | class ControlVelocityPID: 270 | def __init__(self, heater, config): 271 | self.heater = heater 272 | self.heater_max_power = heater.get_max_power() 273 | self.dt = heater.pwm_delay 274 | self.Kp = config.getfloat('pid_Kp') / PID_PARAM_BASE 275 | self.Ki = config.getfloat('pid_Ki') / PID_PARAM_BASE 276 | self.Kd = config.getfloat('pid_Kd') / PID_PARAM_BASE 277 | self.smooth = 1. + heater.get_smooth_time() / self.dt 278 | self.t = [0.] * 3 # temperature readings 279 | self.d1 = 0. # previous 1st derivative 280 | self.d2 = 0. # previous 2nd derivative 281 | self.pwm = 0. 282 | 283 | def temperature_update(self, read_time, temp, target_temp): 284 | self.t.pop(0) 285 | self.t.append(temp) 286 | 287 | # calculate the derivatives using a modified moving average, 288 | # also account for derivative and proportional kick 289 | d1 = self.t[-1] - self.t[-2] 290 | self.d1 = ((self.smooth - 1.) * self.d1 + d1)/self.smooth 291 | d2 = (self.t[-1] - 2.*self.t[-2] + self.t[-3])/self.dt 292 | self.d2 = ((self.smooth - 1.) * self.d2 + d2)/self.smooth 293 | 294 | # calcualte the output 295 | p = self.Kp * -self.d1 296 | i = self.Ki * self.dt * (target_temp - self.t[-1]) 297 | d = self.Kd * -self.d2 298 | self.pwm = max(0., min(self.heater_max_power, self.pwm + p + i + d)) 299 | 300 | # ensure no weird artifacts 301 | if target_temp == 0.: 302 | self.d1 = 0. 303 | self.d2 = 0. 304 | self.pwm = 0. 305 | 306 | # update the heater 307 | self.heater.set_pwm(read_time, self.pwm) 308 | 309 | def check_busy(self, eventtime, smoothed_temp, target_temp): 310 | temp_diff = target_temp - smoothed_temp 311 | return (abs(temp_diff) > PID_SETTLE_DELTA 312 | or abs(self.d1) > PID_SETTLE_SLOPE) 313 | 314 | def get_params(self, gcmd): 315 | gcmd.respond_info( 316 | "PID parameters: pid_Kp=%.3f pid_Ki=%.3f pid_Kd=%.3f" 317 | % (self.Kp * PID_PARAM_BASE, 318 | self.Ki * PID_PARAM_BASE, 319 | self.Kd * PID_PARAM_BASE)) 320 | 321 | def set_params(self, gcmd): 322 | self.Kp = gcmd.get_float('KP', self.Kp * PID_PARAM_BASE) / PID_PARAM_BASE 323 | self.Ki = gcmd.get_float('KI', self.Ki * PID_PARAM_BASE) / PID_PARAM_BASE 324 | self.Kd = gcmd.get_float('KD', self.Kd * PID_PARAM_BASE) / PID_PARAM_BASE 325 | self.get_params(gcmd) 326 | 327 | 328 | ###################################################################### 329 | # Sensor and heater lookup 330 | ###################################################################### 331 | 332 | class PrinterHeaters: 333 | def __init__(self, config): 334 | self.printer = config.get_printer() 335 | self.printer.add_object('heaters', self) 336 | self.sensor_factories = {} 337 | self.heaters = {} 338 | self.gcode_id_to_sensor = {} 339 | self.available_heaters = [] 340 | self.available_sensors = [] 341 | self.has_started = self.have_load_sensors = False 342 | self.printer.register_event_handler("klippy:ready", self._handle_ready) 343 | self.printer.register_event_handler("gcode:request_restart", 344 | self.turn_off_all_heaters) 345 | # Register commands 346 | gcode = self.printer.lookup_object('gcode') 347 | gcode.register_command("TURN_OFF_HEATERS", self.cmd_TURN_OFF_HEATERS, 348 | desc=self.cmd_TURN_OFF_HEATERS_help) 349 | gcode.register_command("M105", self.cmd_M105, when_not_ready=True) 350 | gcode.register_command("TEMPERATURE_WAIT", self.cmd_TEMPERATURE_WAIT, 351 | desc=self.cmd_TEMPERATURE_WAIT_help) 352 | def load_config(self, config): 353 | self.have_load_sensors = True 354 | # Load default temperature sensors 355 | pconfig = self.printer.lookup_object('configfile') 356 | dir_name = os.path.dirname(__file__) 357 | filename = os.path.join(dir_name, 'temperature_sensors.cfg') 358 | try: 359 | dconfig = pconfig.read_config(filename) 360 | except Exception: 361 | raise config.config_error("Cannot load config '%s'" % (filename,)) 362 | for c in dconfig.get_prefix_sections(''): 363 | self.printer.load_object(dconfig, c.get_name()) 364 | def add_sensor_factory(self, sensor_type, sensor_factory): 365 | self.sensor_factories[sensor_type] = sensor_factory 366 | def setup_heater(self, config, gcode_id=None): 367 | heater_name = config.get_name().split()[-1] 368 | if heater_name in self.heaters: 369 | raise config.error("Heater %s already registered" % (heater_name,)) 370 | # Setup sensor 371 | sensor = self.setup_sensor(config) 372 | # Create heater 373 | self.heaters[heater_name] = heater = Heater(config, sensor) 374 | self.register_sensor(config, heater, gcode_id) 375 | self.available_heaters.append(config.get_name()) 376 | return heater 377 | def get_all_heaters(self): 378 | return self.available_heaters 379 | def lookup_heater(self, heater_name): 380 | if heater_name not in self.heaters: 381 | raise self.printer.config_error( 382 | "Unknown heater '%s'" % (heater_name,)) 383 | return self.heaters[heater_name] 384 | def setup_sensor(self, config): 385 | if not self.have_load_sensors: 386 | self.load_config(config) 387 | sensor_type = config.get('sensor_type') 388 | if sensor_type not in self.sensor_factories: 389 | raise self.printer.config_error( 390 | "Unknown temperature sensor '%s'" % (sensor_type,)) 391 | if sensor_type == 'NTC 100K beta 3950': 392 | config.deprecate('sensor_type', 'NTC 100K beta 3950') 393 | return self.sensor_factories[sensor_type](config) 394 | def register_sensor(self, config, psensor, gcode_id=None): 395 | self.available_sensors.append(config.get_name()) 396 | if gcode_id is None: 397 | gcode_id = config.get('gcode_id', None) 398 | if gcode_id is None: 399 | return 400 | if gcode_id in self.gcode_id_to_sensor: 401 | raise self.printer.config_error( 402 | "G-Code sensor id %s already registered" % (gcode_id,)) 403 | self.gcode_id_to_sensor[gcode_id] = psensor 404 | def get_status(self, eventtime): 405 | return {'available_heaters': self.available_heaters, 406 | 'available_sensors': self.available_sensors} 407 | def turn_off_all_heaters(self, print_time=0.): 408 | for heater in self.heaters.values(): 409 | heater.set_temp(0.) 410 | cmd_TURN_OFF_HEATERS_help = "Turn off all heaters" 411 | def cmd_TURN_OFF_HEATERS(self, gcmd): 412 | self.turn_off_all_heaters() 413 | # G-Code M105 temperature reporting 414 | def _handle_ready(self): 415 | self.has_started = True 416 | def _get_temp(self, eventtime): 417 | # Tn:XXX /YYY B:XXX /YYY 418 | out = [] 419 | if self.has_started: 420 | for gcode_id, sensor in sorted(self.gcode_id_to_sensor.items()): 421 | cur, target = sensor.get_temp(eventtime) 422 | out.append("%s:%.1f /%.1f" % (gcode_id, cur, target)) 423 | if not out: 424 | return "T:0" 425 | return " ".join(out) 426 | def cmd_M105(self, gcmd): 427 | # Get Extruder Temperature 428 | reactor = self.printer.get_reactor() 429 | msg = self._get_temp(reactor.monotonic()) 430 | did_ack = gcmd.ack(msg) 431 | if not did_ack: 432 | gcmd.respond_raw(msg) 433 | def _wait_for_temperature(self, heater): 434 | # Helper to wait on heater.check_busy() and report M105 temperatures 435 | if self.printer.get_start_args().get('debugoutput') is not None: 436 | return 437 | toolhead = self.printer.lookup_object("toolhead") 438 | gcode = self.printer.lookup_object("gcode") 439 | reactor = self.printer.get_reactor() 440 | eventtime = reactor.monotonic() 441 | while not self.printer.is_shutdown() and heater.check_busy(eventtime): 442 | print_time = toolhead.get_last_move_time() 443 | gcode.respond_raw(self._get_temp(eventtime)) 444 | eventtime = reactor.pause(eventtime + 1.) 445 | def set_temperature(self, heater, temp, wait=False): 446 | toolhead = self.printer.lookup_object('toolhead') 447 | toolhead.register_lookahead_callback((lambda pt: None)) 448 | heater.set_temp(temp) 449 | if wait and temp: 450 | self._wait_for_temperature(heater) 451 | cmd_TEMPERATURE_WAIT_help = "Wait for a temperature on a sensor" 452 | def cmd_TEMPERATURE_WAIT(self, gcmd): 453 | sensor_name = gcmd.get('SENSOR') 454 | if sensor_name not in self.available_sensors: 455 | raise gcmd.error("Unknown sensor '%s'" % (sensor_name,)) 456 | min_temp = gcmd.get_float('MINIMUM', float('-inf')) 457 | max_temp = gcmd.get_float('MAXIMUM', float('inf'), above=min_temp) 458 | if min_temp == float('-inf') and max_temp == float('inf'): 459 | raise gcmd.error( 460 | "Error on 'TEMPERATURE_WAIT': missing MINIMUM or MAXIMUM.") 461 | if self.printer.get_start_args().get('debugoutput') is not None: 462 | return 463 | if sensor_name in self.heaters: 464 | sensor = self.heaters[sensor_name] 465 | else: 466 | sensor = self.printer.lookup_object(sensor_name) 467 | toolhead = self.printer.lookup_object("toolhead") 468 | reactor = self.printer.get_reactor() 469 | eventtime = reactor.monotonic() 470 | while not self.printer.is_shutdown(): 471 | temp, target = sensor.get_temp(eventtime) 472 | if temp >= min_temp and temp <= max_temp: 473 | return 474 | print_time = toolhead.get_last_move_time() 475 | gcmd.respond_raw(self._get_temp(eventtime)) 476 | eventtime = reactor.pause(eventtime + 1.) 477 | 478 | def load_config(config): 479 | return PrinterHeaters(config) 480 | -------------------------------------------------------------------------------- /pid_calibrate_extra.py: -------------------------------------------------------------------------------- 1 | # Calibration of heater PID settings 2 | # 3 | # Copyright (C) 2016-2018 Kevin O'Connor 4 | # 5 | # This file may be distributed under the terms of the GNU GPLv3 license. 6 | import math, logging 7 | from . import heaters 8 | 9 | class PIDCalibrate: 10 | def __init__(self, config): 11 | self.printer = config.get_printer() 12 | gcode = self.printer.lookup_object('gcode') 13 | gcode.register_command('PID_CALIBRATE_EXTRA', self.cmd_PID_CALIBRATE, 14 | desc=self.cmd_PID_CALIBRATE_help) 15 | cmd_PID_CALIBRATE_help = "Run PID calibration test" 16 | def cmd_PID_CALIBRATE(self, gcmd): 17 | heater_name = gcmd.get('HEATER') 18 | target = gcmd.get_float('TARGET') 19 | write_file = gcmd.get_int('WRITE_FILE', 0) 20 | tolerance = gcmd.get_float('TOLERANCE', TUNE_PID_TOL, above=0.) 21 | pheaters = self.printer.lookup_object('heaters') 22 | try: 23 | heater = pheaters.lookup_heater(heater_name) 24 | except self.printer.config_error as e: 25 | raise gcmd.error(str(e)) 26 | self.printer.lookup_object('toolhead').get_last_move_time() 27 | calibrate = ControlAutoTune(heater, target, tolerance) 28 | old_control = heater.set_control(calibrate) 29 | try: 30 | pheaters.set_temperature(heater, target, True) 31 | except self.printer.command_error as e: 32 | heater.set_control(old_control) 33 | raise 34 | heater.set_control(old_control) 35 | if write_file: 36 | calibrate.write_file('/tmp/heattest.csv') 37 | if calibrate.check_busy(0., 0., 0.): 38 | raise gcmd.error("pid_calibrate interrupted") 39 | # Log and report results 40 | Kp, Ki, Kd = calibrate.calc_pid(gcmd) 41 | logging.info("Autotune: final: Kp=%f Ki=%f Kd=%f", Kp, Ki, Kd) 42 | gcmd.respond_info( 43 | "PID parameters: pid_Kp=%.3f pid_Ki=%.3f pid_Kd=%.3f\n" 44 | "The SAVE_CONFIG command will update the printer config file\n" 45 | "with these parameters and restart the printer." % (Kp, Ki, Kd)) 46 | # Store results for SAVE_CONFIG 47 | configfile = self.printer.lookup_object('configfile') 48 | configfile.set(heater_name, 'control', 'pid') 49 | configfile.set(heater_name, 'pid_Kp', "%.3f" % (Kp,)) 50 | configfile.set(heater_name, 'pid_Ki', "%.3f" % (Ki,)) 51 | configfile.set(heater_name, 'pid_Kd', "%.3f" % (Kd,)) 52 | 53 | TUNE_PID_DELTA = 5.0 54 | TUNE_PID_TOL = 0.02 55 | TUNE_PID_SAMPLES = 3 56 | TUNE_PID_MAX_PEAKS = 60 57 | 58 | class ControlAutoTune: 59 | def __init__(self, heater, target, tolerance): 60 | self.heater = heater 61 | self.heater_max_power = heater.get_max_power() 62 | # store the reference so we can push messages if needed 63 | self.gcode = heater.printer.lookup_object('gcode') 64 | # holds the various max power settings used during the test. 65 | self.powers = [self.heater_max_power] 66 | # holds the times the power setting was changed. 67 | self.times = [] 68 | # the target temperature to tune for 69 | self.target = target 70 | # the tolerance that determines if the system has converged to an 71 | # acceptable level 72 | self.tolerance = tolerance 73 | # the the temp that determines when to turn the heater off 74 | self.temp_high = target + TUNE_PID_DELTA/2. 75 | # the the temp that determines when to turn the heater on 76 | self.temp_low = target - TUNE_PID_DELTA/2. 77 | # is the heater on 78 | self.heating = False 79 | # the current potential peak value 80 | self.peak = self.target 81 | # the time values associated with the current potential peak 82 | self.peak_times = [] 83 | # known peaks and their associated time values 84 | self.peaks = [] 85 | # has the target temp been crossed at-least once 86 | self.target_crossed = False 87 | # has the tuning processed finished 88 | self.done = False 89 | # has the tuning processed started 90 | self.started = False 91 | # did an error happen 92 | self.errored = False 93 | # data from the test that can be optionally written to a file 94 | self.data = [] 95 | def temperature_update(self, read_time, temp, target_temp): 96 | # tuning is done, so don't do any more calculations 97 | if self.done: 98 | return 99 | # store test data 100 | self.data.append((read_time, temp, self.heater.last_pwm_value, 101 | self.target)) 102 | # ensure the starting temp is low enough to run the test. 103 | if not self.started and temp >= self.temp_low: 104 | self.errored = True 105 | self.finish(read_time) 106 | self.gcode.respond_info("temperature to high to start calibration") 107 | return 108 | else: 109 | self.started = True 110 | # ensure the test doesn't run to long 111 | if float(len(self.peaks)) > TUNE_PID_MAX_PEAKS: 112 | self.errored = True 113 | self.finish(read_time) 114 | self.gcode.respond_info("calibration did not finish in time") 115 | return 116 | # indicate that the target temp has been crossed at-least once 117 | if temp > self.target and self.target_crossed == False: 118 | self.target_crossed = True 119 | # only do work if the target temp has been crossed at-least once 120 | if self.target_crossed: 121 | # check for a new peak value 122 | if temp > self.temp_high or temp < self.temp_low : 123 | self.check_peak(read_time, temp) 124 | # it's time to calculate and store a high peak 125 | if self.peak > self.temp_high and temp < self.target: 126 | self.store_peak() 127 | # it's time to calculate and store a low peak 128 | if self.peak < self.temp_low and temp > self.target: 129 | self.store_peak() 130 | # check if the conditions are right to evaluate a new sample 131 | peaks = float(len(self.peaks)) - 1. 132 | powers = float(len(self.powers)) 133 | if (peaks % 2.) == 0. and (powers * 2.) == peaks: 134 | self.log_info() 135 | # check for convergence 136 | if self.converged(): 137 | self.finish(read_time) 138 | return 139 | self.set_power() 140 | # turn the heater off 141 | if self.heating and temp >= self.temp_high: 142 | self.heating = False 143 | self.times.append(read_time) 144 | self.heater.alter_target(self.temp_low) 145 | # turn the heater on 146 | if not self.heating and temp <= self.temp_low: 147 | self.heating = True 148 | self.times.append(read_time) 149 | self.heater.alter_target(self.temp_high) 150 | # set the pwm output based on the heater state 151 | if self.heating: 152 | self.heater.set_pwm(read_time, self.powers[-1]) 153 | else: 154 | self.heater.set_pwm(read_time, 0) 155 | def check_peak(self, time, temp): 156 | # deal with duplicate temps 157 | if temp == self.peak: 158 | self.peak_times.append(time) 159 | # deal with storing high peak values 160 | if temp > self.target and temp > self.peak: 161 | self.peak = temp 162 | self.peak_times = [time] 163 | # deal with storing low peak values 164 | if temp < self.target and temp < self.peak: 165 | self.peak = temp 166 | self.peak_times = [time] 167 | def store_peak(self): 168 | time = sum(self.peak_times)/float(len(self.peak_times)) 169 | self.peaks.append((time, self.peak)) 170 | self.peak = self.target 171 | self.peak_times = [] 172 | def log_info(self): 173 | # provide some useful info to the user 174 | sample = len(self.powers) 175 | pwm = self.powers[-1] 176 | asymmetry = (self.peaks[-2][1] + self.peaks[-1][1])/2. - self.target 177 | tolerance = self.get_sample_tolerance() 178 | if tolerance is False: 179 | fmt = "sample:%d pwm:%.4f asymmetry:%.4f tolerance:n/a\n" 180 | data = (sample, pwm, asymmetry) 181 | self.gcode.respond_info(fmt % data) 182 | else: 183 | fmt = "sample:%d pwm:%.4f asymmetry:%.4f tolerance:%.4f\n" 184 | data = (sample, pwm, asymmetry, tolerance) 185 | self.gcode.respond_info(fmt % data) 186 | def get_sample_tolerance(self): 187 | powers = len(self.powers) 188 | if powers < TUNE_PID_SAMPLES + 1: 189 | return False 190 | powers = self.powers[-1*(TUNE_PID_SAMPLES+1):] 191 | return max(powers)-min(powers) 192 | def converged(self): 193 | tolerance = self.get_sample_tolerance() 194 | if tolerance is False: 195 | return False 196 | if tolerance <= self.tolerance: 197 | return True 198 | return False 199 | def set_power(self): 200 | peak_low = self.peaks[-2][1] 201 | peak_high = self.peaks[-1][1] 202 | power = self.powers[-1] 203 | mid = power * ((self.target - peak_low)/(peak_high - peak_low)) 204 | if mid * 2. > self.heater_max_power: 205 | # the new power is to high so just return max power 206 | self.powers.append(self.heater_max_power) 207 | return 208 | self.powers.append(mid * 2.) 209 | def finish(self, time): 210 | self.heater.set_pwm(time, 0) 211 | self.heater.alter_target(0) 212 | self.done = True 213 | self.heating = False 214 | def check_busy(self, eventtime, smoothed_temp, target_temp): 215 | if eventtime == 0. and smoothed_temp == 0. and target_temp == 0.: 216 | if self.errored: 217 | return True 218 | return False 219 | if self.done: 220 | return False 221 | return True 222 | def write_file(self, filename): 223 | f = open(filename, "w") 224 | f.write('time, temp, pwm, target\n') 225 | data = ["%.5f, %.5f, %.5f, %.5f" % (time, temp, pwm, target) 226 | for time, temp, pwm, target in self.data] 227 | f.write('\n'.join(data)) 228 | peaks = self.peaks[:] 229 | powers = self.powers[:] 230 | # pop off the 231 | peaks.pop(0) 232 | samples = [] 233 | for i in range(len(powers)): 234 | samples.append((i, peaks[i*2][0], peaks[i*2][1], peaks[i*2+1][0], 235 | peaks[i*2+1][1], powers[i])) 236 | f.write('\nsample, low time, low, high time, high, max power\n') 237 | data = ["%.5f, %.5f, %.5f, %.5f, %.5f, %.5f" % (sample, low_time, 238 | low, high_time, high, max_power) for sample, low_time, low, 239 | high_time, high, max_power in samples] 240 | f.write('\n'.join(data)) 241 | f.close() 242 | def calc_pid(self, gcmd): 243 | temp_diff = 0. 244 | time_diff = 0. 245 | theta = 0. 246 | for i in range(1, TUNE_PID_SAMPLES * 2, 2): 247 | temp_diff = temp_diff + self.peaks[-i][1] - self.peaks[-i-1][1] 248 | time_diff = time_diff + self.peaks[-i][0] - self.peaks[-i-2][0] 249 | theta = theta + self.peaks[-i][0] - self.times[-i] 250 | temp_diff = temp_diff/float(TUNE_PID_SAMPLES) 251 | time_diff = time_diff/float(TUNE_PID_SAMPLES) 252 | theta = theta/float(TUNE_PID_SAMPLES) 253 | amplitude = .5 * abs(temp_diff) 254 | power = self.powers[-1*(TUNE_PID_SAMPLES):] 255 | power = sum(power)/float(len(power)) 256 | # calculate the various parameters 257 | Ku = 4. * power / (math.pi * amplitude) 258 | Tu = time_diff 259 | Wu = (2. * math.pi)/Tu 260 | tau = math.tan(math.pi - theta *Wu)/Wu 261 | Km = -math.sqrt(tau**2 * Wu**2 + 1.)/Ku 262 | # log the extra details 263 | logging.info("Ziegler-Nichols constants: Ku=%f Tu=%f", Ku, Tu) 264 | logging.info("Cohen-Coon constants: Km=%f Theta=%f Tau=%f", Km, 265 | theta, tau) 266 | gcmd.respond_info( 267 | "Ziegler-Nichols constants:\n" 268 | "Ku=%.6f Tu=%.6f" % (Ku, Tu)) 269 | gcmd.respond_info( 270 | "Cohen-Coon constants:\n" 271 | "Km=%.6f Theta=%.6f Tau=%.6f" % (Km, theta, tau)) 272 | 273 | pb = heaters.PID_PARAM_BASE 274 | 275 | gcmd.respond_info( 276 | "Classic PID parameters:" 277 | "Kp=%.3f Ki=%.3f Kd=%.3f\n" % (0.6 * Ku * pb, 1.2 * Ku / Tu * pb, 0.075 * Ku * Tu * pb)) 278 | 279 | gcmd.respond_info("Extra PID variants:") 280 | gcmd.respond_info( 281 | "Pessen Integral Rule PID parameters:" 282 | "Kp=%.3f Ki=%.3f Kd=%.3f\n" % (0.7 * Ku * pb, 1.75 * Ku / Tu * pb, 0.105 * Ku * Tu * pb)) 283 | gcmd.respond_info( 284 | "Some Overshoot PID parameters:" 285 | "Kp=%.3f Ki=%.3f Kd=%.3f\n" % (0.333 * Ku * pb, 0.666 * Ku / Tu * pb, 0.111 * Ku * Tu * pb)) 286 | gcmd.respond_info( 287 | "No Overshoot PID parameters:" 288 | "Kp=%.3f Ki=%.3f Kd=%.3f\n" % (0.2 * Ku * pb, 0.4 * Ku / Tu * pb, 0.0666 * Ku * Tu * pb)) 289 | 290 | # Use Ziegler-Nichols method to generate PID parameters 291 | Ti = 0.5 * Tu 292 | Td = 0.125 * Tu 293 | Kp = 0.6 * Ku * pb 294 | Ki = Kp / Ti 295 | Kd = Kp * Td 296 | 297 | return Kp, Ki, Kd 298 | 299 | def load_config(config): 300 | return PIDCalibrate(config) 301 | -------------------------------------------------------------------------------- /rotation_distance.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | class StepperRotationDistance: 5 | def __init__(self, config): 6 | self.printer = config.get_printer() 7 | 8 | gcode = self.printer.lookup_object('gcode') 9 | gcode.register_command("ROTATION_DISTANCE_CALC", 10 | self.cmd_ROTATION_DISTANCE_CALC, 11 | desc=self.cmd_ROTATION_DISTANCE_CALC_help) 12 | gcode.register_command("ROTATION_DISTANCE_SAVE", 13 | self.cmd_ROTATION_DISTANCE_SAVE, 14 | desc=self.cmd_ROTATION_DISTANCE_SAVE_help) 15 | 16 | 17 | cmd_ROTATION_DISTANCE_CALC_help = "Adjust rotation distance value based on calibration" 18 | def cmd_ROTATION_DISTANCE_CALC(self, gcmd): 19 | stepper_name = gcmd.get('EXTRUDER', None) 20 | if stepper_name is None: 21 | gcmd.respond_info('ROTATION_DISTANCE_CALC: Missing EXTRUDER value') 22 | return 23 | if stepper_name == 'extruder': 24 | object_name = 'extruder' 25 | else: 26 | object_name = f'extruder_stepper {stepper_name}' 27 | if object_name in self.printer.objects: 28 | extruder = self.printer.lookup_object(object_name) 29 | stepper = extruder.extruder_stepper.stepper 30 | if stepper is None: 31 | gcmd.respond_info('ROTATION_DISTANCE_CALC: Invalid EXTRUDER value "%s"' 32 | % (stepper_name,)) 33 | return 34 | extruded = gcmd.get_float('EXTRUDED', None) 35 | if extruded is None: 36 | gcmd.respond_info('ROTATION_DISTANCE_CALC: Missing EXTRUDED value') 37 | return 38 | if extruded < 0: 39 | gcmd.respond_info('ROTATION_DISTANCE_CALC: Invalid EXTRUDED value "%f"' 40 | % (extruded,)) 41 | return 42 | requested = gcmd.get_float('REQUESTED', None) 43 | if requested is None: 44 | gcmd.respond_info('ROTATION_DISTANCE_CALC: Missing REQUESTED value') 45 | return 46 | if requested < 0: 47 | gcmd.respond_info('ROTATION_DISTANCE_CALC: Invalid REQUESTED value "%f"' 48 | % (requested,)) 49 | return 50 | distance_current, spr = stepper.get_rotation_distance() 51 | gcmd.respond_info("Extruder '%s' current rotation distance set to %0.6f" 52 | % (stepper_name, distance_current)) 53 | distance_new = distance_current * extruded / requested 54 | toolhead = self.printer.lookup_object('toolhead') 55 | toolhead.flush_step_generation() 56 | stepper.set_rotation_distance(distance_new) 57 | gcmd.respond_info("Extruder '%s' rotation distance set to %0.6f" 58 | % (stepper_name, distance_new)) 59 | 60 | cmd_ROTATION_DISTANCE_SAVE_help = "Save rotation distance to config" 61 | def cmd_ROTATION_DISTANCE_SAVE(self, gcmd): 62 | stepper_name = gcmd.get('EXTRUDER', None) 63 | if stepper_name is None: 64 | gcmd.respond_info('ROTATION_DISTANCE_SAVE: Missing EXTRUDER value') 65 | return 66 | if stepper_name == 'extruder': 67 | object_name = 'extruder' 68 | else: 69 | object_name = f'extruder_stepper {stepper_name}' 70 | if object_name in self.printer.objects: 71 | extruder = self.printer.lookup_object(object_name) 72 | stepper = extruder.extruder_stepper.stepper 73 | if stepper is None: 74 | gcmd.respond_info('ROTATION_DISTANCE_SAVE: Invalid EXTRUDER value "%s"' 75 | % (stepper_name,)) 76 | return 77 | 78 | distance_current, spr = stepper.get_rotation_distance() 79 | configfile = self.printer.lookup_object('configfile') 80 | configfile.set(object_name, 'rotation_distance', distance_current) 81 | 82 | gcmd.respond_info("Extruder '%s' rotation distance set to %0.6f\n" 83 | "The SAVE_CONFIG command will update the printer config file" 84 | % (stepper_name, distance_current)) 85 | 86 | def load_config(config): 87 | return StepperRotationDistance(config) --------------------------------------------------------------------------------