From 1f32367dee580e1c6a88ba74b6272087ab047321 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Tue, 25 Feb 2025 16:46:30 +0100 Subject: [PATCH 1/2] heaters: PID allow adj output with predictive control callback Bound the PWM output early to simplify the prediction logic Signed-off-by: Timofey Titovets --- klippy/extras/heaters.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/klippy/extras/heaters.py b/klippy/extras/heaters.py index ec4792dea..0e082f86b 100644 --- a/klippy/extras/heaters.py +++ b/klippy/extras/heaters.py @@ -46,6 +46,8 @@ class Heater: # pwm caching self.next_pwm_time = 0. self.last_pwm_value = 0. + # Predictive control callback + self.pc_callback = self._pc_def_callback # Setup control algorithm sub-class algos = {'watermark': ControlBangBang, 'pid': ControlPID} algo = config.getchoice('control', algos) @@ -93,6 +95,12 @@ class Heater: self.smoothed_temp += temp_diff * adj_time self.can_extrude = (self.smoothed_temp >= self.min_extrude_temp) #logging.debug("temp: %.3f %f = %f", read_time, temp) + def _pc_def_callback(self): + return .0 + def get_pc_correction(self): + return self.pc_callback() + def set_pc_callback(self, cb): + self.pc_callback = cb def _handle_shutdown(self): self.verify_mainthread_time = -999. # External commands @@ -217,6 +225,8 @@ class ControlPID: temp_integ = max(0., min(self.temp_integ_max, temp_integ)) # Calculate output co = self.Kp*temp_err + self.Ki*temp_integ - self.Kd*temp_deriv + co = max(-1.0 * self.heater_max_power, min(self.heater_max_power, co)) + co += self.heater.get_pc_correction() #logging.debug("pid: %f@%.3f -> diff=%f deriv=%f err=%f integ=%f co=%d", # temp, read_time, temp_diff, temp_deriv, temp_err, temp_integ, co) bounded_co = max(0., min(self.heater_max_power, co)) From 2517b470618915aa955b29811949544390555847 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Tue, 25 Feb 2025 19:45:18 +0100 Subject: [PATCH 2/2] heater_pc: define predictive control extras Run the template in the reactor, that way, status access will be thread-safe. Test template on "connect", to avoid a crash within the reactor's timer. Signed-off-by: Timofey Titovets --- docs/Config_Reference.md | 11 ++++++ klippy/extras/heater_pc.py | 71 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 klippy/extras/heater_pc.py diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 8b17d9502..5fddff7ce 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -5515,6 +5515,17 @@ cs_pin: # above parameters. ``` +### [heater_pc] + +Heater prediction correction. +To use this feature, define a config section with a "heater_pc" prefix +followed by the name of the corresponding heater config section. +For example `[heater_pc heater_bed]` +``` +[heater_pc extruder] +#macro_template: +``` + ## Common bus parameters ### Common SPI settings diff --git a/klippy/extras/heater_pc.py b/klippy/extras/heater_pc.py new file mode 100644 index 000000000..eb40cf132 --- /dev/null +++ b/klippy/extras/heater_pc.py @@ -0,0 +1,71 @@ +# Klipper Heater Predictional Control +# +# Copyright (C) 2025 Timofey Titovets +# +# This file may be distributed under the terms of the GNU GPLv3 license. + +import threading +from .gcode_macro import PrinterGCodeMacro +from .display import display + +class HeaterPredictControl: + def __init__(self, config): + self.printer = config.get_printer() + self.reactor = self.printer.get_reactor() + self.config = config + self.eval_time = 0.3 + self.min_pwm = -1.0 + self.max_pwm = 1.0 + self.render_timer = None + name_parts = config.get_name().split() + if len(name_parts) != 2: + raise config.error("Section name '%s' is not valid" + % (config.get_name(),)) + # Use lock to pass data to/from heater code + self.lock = threading.Lock() + self.output = .0 + self.pwm_event_time = self.reactor.monotonic() + # Link template + template_name = config.get("macro_template") + templates = display.lookup_display_templates(config) + display_templates = templates.get_display_templates() + self.create_context = PrinterGCodeMacro(config).create_template_context + self.template = display_templates.get(template_name) + self.printer.register_event_handler("klippy:connect", + self.handle_connect) + def handle_connect(self): + pheaters = self.printer.load_object(self.config, 'heaters') + heater_name = self.config.get_name().split()[-1] + heater = pheaters.heaters.get(heater_name) + if heater is None: + self.config.error("Heater %s is not registered" % (heater_name)) + self.eval_time = heater.get_pwm_delay() + self.min_pwm = -1.0 * heater.get_max_power() + self.max_pwm = heater.get_max_power() + reactor = self.reactor + self.render_timer = reactor.register_timer(self._render, reactor.NOW) + def callback(): + output = .0 + with self.lock: + self.pwm_event_time = self.reactor.monotonic() + output = self.output + return output + heater.set_pc_callback(callback) + # Test template + self._render(.0) + def _render(self, eventtime): + context = self.create_context() + output = self.template.render(context) + # Normalize output to PWM limits + output_f = float(output) * self.max_pwm + output_f = max(self.min_pwm, min(self.max_pwm, output_f)) + with self.lock: + self.output = output_f + last_pwm = self.pwm_event_time + # if we lag behind - reschedule + if eventtime < last_pwm + self.eval_time * 3 / 4: + return last_pwm + self.eval_time * 3 / 4 + return eventtime + self.eval_time + +def load_config_prefix(config): + return HeaterPredictControl(config)