mirror of
https://github.com/Klipper3d/klipper.git
synced 2026-01-06 14:57:53 -07:00
Merge 67b8d7e818 into dc622f4ac3
This commit is contained in:
commit
1bb253650f
3 changed files with 133 additions and 31 deletions
|
|
@ -341,6 +341,22 @@ class ConfigAutoSave:
|
|||
del pending[section]
|
||||
self.status_save_pending = pending
|
||||
self.save_config_pending = True
|
||||
def remove_option(self, section, option):
|
||||
if (self.fileconfig.has_section(section) and
|
||||
self.fileconfig.has_option(section, option)):
|
||||
self.fileconfig.remove_option(section, option)
|
||||
pending = dict(self.status_save_pending)
|
||||
sect_pending = pending.get(section)
|
||||
if sect_pending is not None:
|
||||
sect_dict = dict(sect_pending) if sect_pending else {}
|
||||
if option in sect_dict:
|
||||
del sect_dict[option]
|
||||
if sect_dict:
|
||||
pending[section] = sect_dict
|
||||
else:
|
||||
pending.pop(section, None)
|
||||
self.status_save_pending = pending
|
||||
self.save_config_pending = True
|
||||
def _disallow_include_conflicts(self, regular_fileconfig):
|
||||
for section in self.fileconfig.sections():
|
||||
for option in self.fileconfig.options(section):
|
||||
|
|
@ -535,3 +551,5 @@ class PrinterConfig:
|
|||
self.autosave.set(section, option, value)
|
||||
def remove_section(self, section):
|
||||
self.autosave.remove_section(section)
|
||||
def remove_option(self, section, option):
|
||||
self.autosave.remove_option(section, option)
|
||||
|
|
|
|||
|
|
@ -191,17 +191,58 @@ class ControlPID:
|
|||
def __init__(self, heater, config):
|
||||
self.heater = heater
|
||||
self.heater_max_power = heater.get_max_power()
|
||||
self.Kp = config.getfloat('pid_Kp') / PID_PARAM_BASE
|
||||
self.Ki = config.getfloat('pid_Ki') / PID_PARAM_BASE
|
||||
self.Kd = config.getfloat('pid_Kd') / PID_PARAM_BASE
|
||||
pid_options = [opt for opt in config.get_prefix_options('pid_table_')]
|
||||
if pid_options:
|
||||
if len(pid_options) < 2:
|
||||
raise config.error(
|
||||
"Adaptive PID requires at least two pid_table_ entries")
|
||||
self.adaptive = True
|
||||
self.pid_table = []
|
||||
for option in sorted(pid_options):
|
||||
value = config.get(option)
|
||||
parts = value.split(':')
|
||||
if len(parts) != 4:
|
||||
raise config.error(
|
||||
"Invalid %s format, expected temp:Kp:Ki:Kd" % option)
|
||||
temp = float(parts[0])
|
||||
kp = float(parts[1]) / PID_PARAM_BASE
|
||||
ki = float(parts[2]) / PID_PARAM_BASE
|
||||
kd = float(parts[3]) / PID_PARAM_BASE
|
||||
self.pid_table.append((temp, kp, ki, kd))
|
||||
|
||||
self.pid_table.sort(key=lambda x: x[0])
|
||||
else:
|
||||
self.adaptive = False
|
||||
self.Kp = config.getfloat('pid_Kp') / PID_PARAM_BASE
|
||||
self.Ki = config.getfloat('pid_Ki') / PID_PARAM_BASE
|
||||
self.Kd = config.getfloat('pid_Kd') / PID_PARAM_BASE
|
||||
self.temp_integ_max = 0.
|
||||
if self.Ki:
|
||||
self.temp_integ_max = self.heater_max_power / self.Ki
|
||||
self.min_deriv_time = heater.get_smooth_time()
|
||||
self.temp_integ_max = 0.
|
||||
if self.Ki:
|
||||
self.temp_integ_max = self.heater_max_power / self.Ki
|
||||
self.prev_temp = AMBIENT_TEMP
|
||||
self.prev_temp_time = 0.
|
||||
self.prev_temp_deriv = 0.
|
||||
self.prev_temp_integ = 0.
|
||||
def _get_pid_params(self, target_temp):
|
||||
if not self.adaptive:
|
||||
return self.Kp, self.Ki, self.Kd
|
||||
|
||||
for i in range(len(self.pid_table) - 1):
|
||||
t1, kp1, ki1, kd1 = self.pid_table[i]
|
||||
t2, kp2, ki2, kd2 = self.pid_table[i + 1]
|
||||
|
||||
if t1 <= target_temp <= t2:
|
||||
ratio = (target_temp - t1) / (t2 - t1)
|
||||
return (
|
||||
kp1 + (kp2 - kp1) * ratio,
|
||||
ki1 + (ki2 - ki1) * ratio,
|
||||
kd1 + (kd2 - kd1) * ratio
|
||||
)
|
||||
|
||||
if target_temp < self.pid_table[0][0]:
|
||||
return self.pid_table[0][1:]
|
||||
return self.pid_table[-1][1:]
|
||||
def temperature_update(self, read_time, temp, target_temp):
|
||||
time_diff = read_time - self.prev_temp_time
|
||||
# Calculate change of temperature
|
||||
|
|
@ -214,9 +255,15 @@ class ControlPID:
|
|||
# Calculate accumulated temperature "error"
|
||||
temp_err = target_temp - temp
|
||||
temp_integ = self.prev_temp_integ + temp_err * time_diff
|
||||
temp_integ = max(0., min(self.temp_integ_max, temp_integ))
|
||||
if self.adaptive:
|
||||
temp_integ_max = self.heater_max_power / Ki if Ki else 0.
|
||||
else:
|
||||
temp_integ_max = self.temp_integ_max
|
||||
temp_integ = max(0., min(temp_integ_max, temp_integ))
|
||||
# Get PID parameters (fixed or interpolated)
|
||||
Kp, Ki, Kd = self._get_pid_params(target_temp)
|
||||
# Calculate output
|
||||
co = self.Kp*temp_err + self.Ki*temp_integ - self.Kd*temp_deriv
|
||||
co = Kp*temp_err + Ki*temp_integ - Kd*temp_deriv
|
||||
#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))
|
||||
|
|
@ -232,7 +279,6 @@ class ControlPID:
|
|||
return (abs(temp_diff) > PID_SETTLE_DELTA
|
||||
or abs(self.prev_temp_deriv) > PID_SETTLE_SLOPE)
|
||||
|
||||
|
||||
######################################################################
|
||||
# Sensor and heater lookup
|
||||
######################################################################
|
||||
|
|
|
|||
|
|
@ -15,7 +15,20 @@ class PIDCalibrate:
|
|||
cmd_PID_CALIBRATE_help = "Run PID calibration test"
|
||||
def cmd_PID_CALIBRATE(self, gcmd):
|
||||
heater_name = gcmd.get('HEATER')
|
||||
target = gcmd.get_float('TARGET')
|
||||
targets = []
|
||||
|
||||
if gcmd.get('TARGETS', None) is not None:
|
||||
targets = [
|
||||
float(x)
|
||||
for x in gcmd.get('TARGETS').split(',')
|
||||
if x.strip()
|
||||
][:20]
|
||||
|
||||
if len(targets)<2:
|
||||
raise gcmd.error("You must specify minimum two temp targets")
|
||||
else:
|
||||
targets = [gcmd.get_float('TARGET')]
|
||||
|
||||
write_file = gcmd.get_int('WRITE_FILE', 0)
|
||||
pheaters = self.printer.lookup_object('heaters')
|
||||
try:
|
||||
|
|
@ -23,32 +36,57 @@ class PIDCalibrate:
|
|||
except self.printer.config_error as e:
|
||||
raise gcmd.error(str(e))
|
||||
self.printer.lookup_object('toolhead').get_last_move_time()
|
||||
calibrate = ControlAutoTune(heater, target)
|
||||
old_control = heater.set_control(calibrate)
|
||||
try:
|
||||
pheaters.set_temperature(heater, target, True)
|
||||
except self.printer.command_error as e:
|
||||
results = []
|
||||
for target in targets:
|
||||
calibrate = ControlAutoTune(heater, target)
|
||||
old_control = heater.set_control(calibrate)
|
||||
try:
|
||||
pheaters.set_temperature(heater, target, True)
|
||||
except self.printer.command_error as e:
|
||||
heater.set_control(old_control)
|
||||
raise
|
||||
heater.set_control(old_control)
|
||||
raise
|
||||
heater.set_control(old_control)
|
||||
if write_file:
|
||||
calibrate.write_file('/tmp/heattest.txt')
|
||||
if calibrate.check_busy(0., 0., 0.):
|
||||
raise gcmd.error("pid_calibrate interrupted")
|
||||
# Log and report results
|
||||
Kp, Ki, Kd = calibrate.calc_final_pid()
|
||||
logging.info("Autotune: final: Kp=%f Ki=%f Kd=%f", Kp, Ki, Kd)
|
||||
gcmd.respond_info(
|
||||
"PID parameters: pid_Kp=%.3f pid_Ki=%.3f pid_Kd=%.3f\n"
|
||||
"The SAVE_CONFIG command will update the printer config file\n"
|
||||
"with these parameters and restart the printer." % (Kp, Ki, Kd))
|
||||
if write_file:
|
||||
calibrate.write_file('/tmp/heattest_%.0f.txt' % target)
|
||||
if calibrate.check_busy(0., 0., 0.):
|
||||
raise gcmd.error("pid_calibrate interrupted")
|
||||
|
||||
Kp, Ki, Kd = calibrate.calc_final_pid()
|
||||
results.append((target, Kp, Ki, Kd))
|
||||
|
||||
# Store results for SAVE_CONFIG
|
||||
cfgname = heater.get_name()
|
||||
configfile = self.printer.lookup_object('configfile')
|
||||
configfile.set(cfgname, 'control', 'pid')
|
||||
configfile.set(cfgname, 'pid_Kp', "%.3f" % (Kp,))
|
||||
configfile.set(cfgname, 'pid_Ki', "%.3f" % (Ki,))
|
||||
configfile.set(cfgname, 'pid_Kd', "%.3f" % (Kd,))
|
||||
if len(results) == 1:
|
||||
configfile.set(cfgname, 'pid_Kp', "%.3f" % (Kp,))
|
||||
configfile.set(cfgname, 'pid_Ki', "%.3f" % (Ki,))
|
||||
configfile.set(cfgname, 'pid_Kd', "%.3f" % (Kd,))
|
||||
else:
|
||||
for i in range(len(results), 20):
|
||||
configfile.remove_option(cfgname, 'pid_table_%d' % i)
|
||||
|
||||
for i, (temp, Kp, Ki, Kd) in enumerate(results):
|
||||
configfile.set(cfgname, 'pid_table_%d' % i,
|
||||
"%.1f:%.3f:%.3f:%.3f" % (temp, Kp, Ki, Kd))
|
||||
|
||||
# Log and report results
|
||||
msg_lines = []
|
||||
for i, (temp, Kp, Ki, Kd) in enumerate(results):
|
||||
logging.info(
|
||||
"Autotune: %.1f°C: Kp=%.3f Ki=%.3f Kd=%.3f",
|
||||
temp, Kp, Ki, Kd
|
||||
)
|
||||
msg_lines.append(
|
||||
"%.0f°C: Kp=%.3f Ki=%.3f Kd=%.3f" % (temp, Kp, Ki, Kd)
|
||||
)
|
||||
|
||||
gcmd.respond_info(
|
||||
"PID parameters:\n%s\n"
|
||||
"The SAVE_CONFIG command will update the printer config file with these "
|
||||
"parameters and restart the printer."
|
||||
% '\n'.join(msg_lines)
|
||||
)
|
||||
|
||||
TUNE_PID_DELTA = 5.0
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue