heater_pc: define predictive control extras

Allows for the amendment of the real control loop
with feed-forward control

Feed-forward allows for the implementation of advanced control,
such as "dual loop", using data from several temperature sensors.
Increase/decrease power beforehand without PID reactive lag.

Templating happens in the reactor,
to make access to the status fields thread-safe.

Template tested on "connect",
So it will crash early and not inside the timer.

Signed-off-by: Timofey Titovets <nefelim4ag@gmail.com>
This commit is contained in:
Timofey Titovets 2025-02-25 19:45:18 +01:00
parent 1b2f2bca64
commit aa4484eeae
2 changed files with 90 additions and 0 deletions

View file

@ -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: <display_template's name>
```
## Common bus parameters
### Common SPI settings

View file

@ -0,0 +1,79 @@
# Klipper Heater Predictional Control
#
# Copyright (C) 2025 Timofey Titovets <nefelim4ag@gmail.com>
#
# 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
self.old_control = 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 = -heater.get_max_power()
self.max_pwm = heater.get_max_power()
reactor = self.reactor
self.render_timer = reactor.register_timer(self._render, reactor.NOW)
self.old_control = heater.set_control(self)
# Test template
self._render(.0)
def temperature_update(self, read_time, temp, target_temp):
output = .0
with self.lock:
self.pwm_event_time = self.reactor.monotonic()
output = self.output
# Returns +- max_power
co = self.old_control.temperature_update(read_time, temp, target_temp)
co += output
return co
def check_busy(self, eventtime, smoothed_temp, target_temp):
res = self.old_control.check_busy(eventtime, smoothed_temp,
target_temp)
return res
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)