This commit is contained in:
Timofey Titovets 2026-02-07 22:15:02 +00:00 committed by GitHub
commit 5b24759f9f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 92 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,71 @@
# 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
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)

View file

@ -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))