From c174a5b0c7fce4de655f7dd8c43c39532a7977c0 Mon Sep 17 00:00:00 2001 From: Franklyn Tackitt Date: Sat, 20 Dec 2025 13:47:12 -0700 Subject: [PATCH 1/2] Move autosave config to printer.autosave.cfg This no longer edits the user's configuration to comment out changes to configuration, and will automatically migrate legacy autosave data to the new .autosave.cfg file Autosave configuration takes priority over matching options in user configuration. --- klippy/configfile.py | 152 +++++------------- klippy/extras/danger_options.py | 16 +- klippy/extras/telemetry.py | 1 - test/klippy/danger_options.cfg | 1 - test/test_autosave.py | 8 +- test/test_configs/autosave/danger_options.cfg | 1 - 6 files changed, 61 insertions(+), 118 deletions(-) diff --git a/klippy/configfile.py b/klippy/configfile.py index 691f1eb883..8d5482fbe4 100644 --- a/klippy/configfile.py +++ b/klippy/configfile.py @@ -10,11 +10,11 @@ import os import pathlib import re +import shutil import sys import time from . import mathutil -from .extras.danger_options import get_danger_options error = configparser.Error @@ -332,7 +332,7 @@ def deprecate(self, option, value=None): pconfig.deprecate(self.section, option, value, msg) -AUTOSAVE_HEADER = """ +AUTOSAVE_HEADER = """\ #*# <---------------------- SAVE_CONFIG ----------------------> #*# DO NOT EDIT THIS BLOCK OR BELOW. The contents are auto-generated. #*# @@ -364,6 +364,18 @@ def __init__(self, printer): def get_printer(self): return self.printer + @property + def main_config_path(self): + return pathlib.Path(self.printer.get_start_args()["config_file"]) + + @property + def autosave_path(self): + return self.main_config_path.with_suffix(".autosave.cfg") + + @staticmethod + def _normalize_newlines(s: str) -> str: + return s.replace("\r\n", "\n") + def _read_config_file(self, filename): try: f = open(filename, "r") @@ -373,7 +385,7 @@ def _read_config_file(self, filename): msg = "Unable to open config file %s" % (filename,) logging.exception(msg) raise error(msg) - return data.replace("\r\n", "\n") + return self._normalize_newlines(data) def _find_autosave_data(self, data): regular_data = data @@ -518,11 +530,19 @@ def read_config(self, filename): ) def read_main_config(self): - filename = self.printer.get_start_args()["config_file"] + filename = self.main_config_path data = self._read_config_file(filename) regular_data, autosave_data = self._find_autosave_data(data) - regular_config = self._build_config_wrapper(regular_data, filename) - autosave_data = self._strip_duplicates(autosave_data, regular_config) + if autosave_data: + self.save_config_pending = True + self.runtime_warning( + f"Kalico has migrated saved configuration data to a separate file {self.autosave_path.name}\n" + f"To migrate your configuration, run `SAVE_CONFIG`" + ) + if self.autosave_path.exists(): + autosave_data += self._normalize_newlines( + self.autosave_path.read_text() + ) self.autosave = self._build_config_wrapper(autosave_data, filename) cfg = self._build_config_wrapper(regular_data + autosave_data, filename) return cfg @@ -657,144 +677,60 @@ def remove_section(self, section): self.status_save_pending = pending self.save_config_pending = True - def _disallow_include_conflicts(self, regular_data, cfgname, gcode): - config = self._build_config_wrapper(regular_data, cfgname) - for section in self.autosave.fileconfig.sections(): - for option in self.autosave.fileconfig.options(section): - if config.fileconfig.has_option(section, option): - # They conflict only if they are not the same value - included_value = config.fileconfig.get(section, option) - autosave_value = self.autosave.fileconfig.get( - section, option - ) - if included_value != autosave_value: - msg = ( - "SAVE_CONFIG section '%s' option '%s' value '%s' conflicts " - "with included value '%s' " - % (section, option, autosave_value, included_value) - ) - raise gcode.error(msg) - cmd_SAVE_CONFIG_help = "Overwrite config file and restart" def _write_backup(self, cfgpath, cfgdata, gcode): - printercfg = self.printer.get_start_args()["config_file"] - configdir = os.path.dirname(printercfg) + configdir = self.main_config_path.parent # Define a directory for configuration backups so that include blocks # using a wildcard to reference all files in a directory don't throw # errors - backupdir = os.path.join(configdir, "config_backups") + backupdir = configdir / "config_backups" # Create the backup directory if it doesn't already exist - if not os.path.exists(backupdir): - os.mkdir(backupdir) + backupdir.mkdir(exist_ok=True) # Generate the name of the backup file by stripping the leading path in # `cfgpath` and appending to it. Then add it to the config_backups dir - datestr = time.strftime("-%Y%m%d_%H%M%S") - cfgname = os.path.basename(cfgpath) - backup_path = backupdir + "/" + cfgname + datestr - if cfgpath.endswith(".cfg"): - backup_path = backupdir + "/" + cfgname[:-4] + datestr + ".cfg" + datestr = time.strftime("%Y%m%d_%H%M%S") + backup_path = (backupdir / self.main_config_path.name).with_stem( + f"{self.main_config_path.stem}-{datestr}" + ) logging.info( "SAVE_CONFIG to '%s' (backup in '%s')", cfgpath, backup_path ) try: # Read the current config into the backup before making changes to # the original file - currentconfig = open(cfgpath, "r") - backupconfig = open(backup_path, "w") - backupconfig.write(currentconfig.read()) - backupconfig.close() - currentconfig.close() + if cfgpath.exists(): + shutil.copy(cfgpath, backup_path) # With the backup created, write the new data to the original file - currentconfig = open(cfgpath, "w") - currentconfig.write(cfgdata) - currentconfig.close() + cfgpath.write_text(cfgdata) except: msg = "Unable to write config file during SAVE_CONFIG" logging.exception(msg) raise gcode.error(msg) - def _save_includes(self, cfgpath, data, visitedpaths, gcode): - # Prevent an infinite loop in the event of configs circularly - # referencing each other - if cfgpath in visitedpaths: - return - - visitedpaths.add(cfgpath) - dirname = os.path.dirname(cfgpath) - # Read the data as individual lines so we can find include blocks - lines = data.split("\n") - for line in lines: - # Strip trailing comment - pos = line.find("#") - if pos >= 0: - line = line[:pos] - - mo = configparser.RawConfigParser.SECTCRE.match(line) - header = mo and mo.group("header") - if header and header.startswith("include "): - include_spec = header[8:].strip() - include_glob = os.path.join(dirname, include_spec) - # retrieve all filenames associated with the absolute path of - # the include header - include_filenames = glob.glob(include_glob) - if not include_filenames and not glob.has_magic(include_glob): - # Empty set is OK if wildcard but not for direct file - # reference - raise error( - "Include file '%s' does not exist" % (include_glob,) - ) - include_filenames.sort() - # Read the include files and check them against autosave data. - # If autosave data overwites anything we'll update the file - # and create a backup. - for include_filename in include_filenames: - # Recursively check for includes. No need to check for looping - # includes as klipper checks this at startup. - include_predata = self._read_config_file(include_filename) - self._save_includes( - include_filename, include_predata, visitedpaths, gcode - ) - - include_postdata = self._strip_duplicates( - include_predata, self.autosave - ) - # Only write and backup data that's been changed - if include_predata != include_postdata: - self._write_backup( - include_filename, include_postdata, gcode - ) - def cmd_SAVE_CONFIG(self, gcmd): if not self.autosave.fileconfig.sections(): return gcode = self.printer.lookup_object("gcode") # Create string containing autosave data - autosave_data = self._build_config_string(self.autosave) - lines = [("#*# " + l).strip() for l in autosave_data.split("\n")] - lines.insert(0, "\n" + AUTOSAVE_HEADER.rstrip()) - lines.append("") - autosave_data = "\n".join(lines) + autosave_data = AUTOSAVE_HEADER + autosave_data += self._build_config_string(self.autosave) # Read in and validate current config file - cfgname = self.printer.get_start_args()["config_file"] + cfgname = self.main_config_path try: data = self._read_config_file(cfgname) - regular_data, old_autosave_data = self._find_autosave_data(data) - config = self._build_config_wrapper(regular_data, cfgname) + regular_data, _ = self._find_autosave_data(data) except error as e: msg = "Unable to parse existing config on SAVE_CONFIG" logging.exception(msg) raise gcode.error(msg) - regular_data = self._strip_duplicates(regular_data, self.autosave) - if get_danger_options().autosave_includes: - self._save_includes(cfgname, data, set(), gcode) + self._write_backup(self.autosave_path, autosave_data, gcode) - # NOW we're safe to check for conflicts - self._disallow_include_conflicts(regular_data, cfgname, gcode) - data = regular_data.rstrip() + autosave_data - self._write_backup(cfgname, data, gcode) + # Migration to remove legacy autosave data + if regular_data != data: + self._write_backup(cfgname, regular_data, gcode) # If requested restart or no restart just flag config saved require_restart = gcmd.get_int("RESTART", 1, minval=0, maxval=1) diff --git a/klippy/extras/danger_options.py b/klippy/extras/danger_options.py index 554f2978fb..53e1fd7e0c 100644 --- a/klippy/extras/danger_options.py +++ b/klippy/extras/danger_options.py @@ -1,5 +1,13 @@ +from __future__ import annotations + +import typing + +if typing.TYPE_CHECKING: + from klippy.configfile import ConfigWrapper + + class DangerOptions: - def __init__(self, config): + def __init__(self, config: ConfigWrapper): self.minimal_logging = config.getboolean("minimal_logging", False) verbose = not self.minimal_logging self.log_statistics = config.getboolean("log_statistics", verbose) @@ -50,7 +58,6 @@ def __init__(self, config): "temp_ignore_limits", temp_ignore_limits ) - self.autosave_includes = config.getboolean("autosave_includes", False) self.bgflush_extra_time = config.getfloat( "bgflush_extra_time", 0.250, minval=0.0 ) @@ -64,6 +71,9 @@ def __init__(self, config): "endstop_sample_count", 4, minval=1 ) + if config.get("autosave_includes", None, False) is not None: + config.deprecate("autosave_includes") + DANGER_OPTIONS: DangerOptions = None @@ -75,7 +85,7 @@ def get_danger_options(): return DANGER_OPTIONS -def load_config(config): +def load_config(config: ConfigWrapper): global DANGER_OPTIONS DANGER_OPTIONS = DangerOptions(config) return DANGER_OPTIONS diff --git a/klippy/extras/telemetry.py b/klippy/extras/telemetry.py index c53bb4ac88..31ec03126a 100644 --- a/klippy/extras/telemetry.py +++ b/klippy/extras/telemetry.py @@ -157,7 +157,6 @@ def _collect_anonymized_config(self): { "danger_options": [ - "autosave_includes", "allow_plugin_override", "log_bed_mesh_at_startup" ], diff --git a/test/klippy/danger_options.cfg b/test/klippy/danger_options.cfg index ee7a03b782..5cb306b8a7 100644 --- a/test/klippy/danger_options.cfg +++ b/test/klippy/danger_options.cfg @@ -17,7 +17,6 @@ log_shutdown_info: False allow_plugin_override: True multi_mcu_trsync_timeout: 0.05 temp_ignore_limits: True -autosave_includes: True bgflush_extra_time: 0.250 [stepper_x] diff --git a/test/test_autosave.py b/test/test_autosave.py index ddd0492a21..79d9164e31 100644 --- a/test/test_autosave.py +++ b/test/test_autosave.py @@ -35,11 +35,11 @@ def test_autosave_includes( # Test that the autosave line is now in printer.cfg assert ( - "#*# temp_ignore_limits = False" - in (config_root / "printer.cfg").read_text() + "temp_ignore_limits = False" + in (config_root / "printer.autosave.cfg").read_text() ) - # Test that the source line in danger_options.cfg is commented out + # Test that the source line in danger_options.cfg is not commented out assert ( - "#temp_ignore_limits: True" + "\ntemp_ignore_limits: True\n" in (config_root / "danger_options.cfg").read_text() ) diff --git a/test/test_configs/autosave/danger_options.cfg b/test/test_configs/autosave/danger_options.cfg index 0430aee44b..ff682e01f6 100644 --- a/test/test_configs/autosave/danger_options.cfg +++ b/test/test_configs/autosave/danger_options.cfg @@ -7,5 +7,4 @@ log_shutdown_info: False allow_plugin_override: True multi_mcu_trsync_timeout: 0.05 temp_ignore_limits: True -autosave_includes: True bgflush_extra_time: 0.250 \ No newline at end of file From 57c164fadfa6962a2f6e4396b11bb208e5508771 Mon Sep 17 00:00:00 2001 From: Franklyn Tackitt Date: Sat, 20 Dec 2025 13:49:23 -0700 Subject: [PATCH 2/2] Update documentation for autosave changes --- docs/Config_Changes.md | 3 +++ docs/Config_Reference.md | 4 ---- docs/Config_checks.md | 4 ++-- docs/Kalico_Additions.md | 1 + docs/MPC.md | 18 ++++++++---------- 5 files changed, 14 insertions(+), 16 deletions(-) diff --git a/docs/Config_Changes.md b/docs/Config_Changes.md index 57798bc761..3c924c24d4 100644 --- a/docs/Config_Changes.md +++ b/docs/Config_Changes.md @@ -8,6 +8,9 @@ All dates in this document are approximate. ## Changes +20251220: The `[danger_options]` section option `autosave_includes` has been +removed + 20250817: The gcode_button section adds a new option `debounce_delay` that takes a time in seconds to debounce the state of the button before any action is taken. It defaults to 0 which causes it to act as if there is no debouncing. diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index a089451249..1f84d42a51 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -108,10 +108,6 @@ A collection of Kalico-specific system options # limits for temperature sensors. It prevents shutdowns due to # 'ADC out of range' and similar errors by allowing readings outside the # specified range without triggering a shutdown. The default is False. -#autosave_includes: False -# When set to true, SAVE_CONFIG will recursively read [include ...] blocks -# for conflicts to autosave data. Any configurations updated will be backed -# up to configs/config_backups. #bgflush_extra_time: 0.250 # This allows to set extra flush time (in seconds). Under certain conditions, # a low value will result in an error if message is not get flushed, a high value diff --git a/docs/Config_checks.md b/docs/Config_checks.md index 85c6ba5409..5531cb8dd3 100644 --- a/docs/Config_checks.md +++ b/docs/Config_checks.md @@ -135,8 +135,8 @@ To calibrate the extruder, navigate to the command console and run the PID_CALIBRATE command. For example: `PID_CALIBRATE HEATER=extruder TARGET=170` -At the completion of the tuning test run `SAVE_CONFIG` to update the -printer.cfg file the new PID settings. +At the completion of the tuning test run `SAVE_CONFIG` to update +printer.autosave.cfg file the new PID settings. If the printer has a heated bed and it supports being driven by PWM (Pulse Width Modulation) then it is recommended to use PID control for diff --git a/docs/Kalico_Additions.md b/docs/Kalico_Additions.md index d89457b28b..d227d49672 100644 --- a/docs/Kalico_Additions.md +++ b/docs/Kalico_Additions.md @@ -2,6 +2,7 @@ ## Changes to Klipper defaults +- `SAVE_CONFIG` no longer edits your configuration, instead maintaining a separate `printer.autosave.cfg`. - [`[force_move]`](./Config_Reference.md#force_move) is enabled by default. Use `[force_move] enable_force_move: False` to disable it - [`[respond]`](./Config_Reference.md#respond) is enabled by default. Use `[respond] enable_respond: False` to disable it - [`[exclude_object]`](./Config_Reference.md#exclude_object) is enabled by default. Use `[exclude_object] enable_exclude_object: False` to disable it diff --git a/docs/MPC.md b/docs/MPC.md index dd64f4609a..40311eb539 100644 --- a/docs/MPC.md +++ b/docs/MPC.md @@ -197,22 +197,20 @@ After successful calibration the method will generate the key model parameters ![Calibration Parameter Output](img/MPC_calibration_output.png) -A `SAVE_CONFIG` command is then required to commit these calibrated model parameters to the printer config or the user can manually update the values. The _SAVE_CONFIG_ block should then look like: +A `SAVE_CONFIG` command is then required to commit these calibrated model parameters to the printer config or the user can manually update the values. Your `printer.autosave.cfg` should then look like: ``` #*# <----------- SAVE_CONFIG -----------> #*# DO NOT EDIT THIS BLOCK OR BELOW. The contents are auto-generated. -#*# [extruder] -#*# control = mpc -#*# block_heat_capacity = 22.3110 -#*# sensor_responsiveness = 0.0998635 -#*# ambient_transfer = 0.155082 -#*# fan_ambient_transfer=0.155082, 0.20156, 0.216441 +#*# +[extruder] +control = mpc +block_heat_capacity = 22.3110 +sensor_responsiveness = 0.0998635 +ambient_transfer = 0.155082 +fan_ambient_transfer=0.155082, 0.20156, 0.216441 ``` -> [!NOTE] -> If the [extruder] section is in a .cfg file other than printer.cfg the `SAVE_CONFIG` command may not be able to write the calibration parameters and klippy will provide an error. - These model parameters are not suitable for pre-configuration or are not explicitly determinable. Advanced users could tweak these post calibration based on the following guidance: Slightly increasing these values will increase the temperature where MPC settles and slightly decreasing them will decrease the settling temperature. - `block_heat_capacity:`