This commit is contained in:
minicx 2025-12-19 21:54:15 +01:00 committed by GitHub
commit 1bb253650f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 133 additions and 31 deletions

View file

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

View file

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

View file

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