Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/Config_Changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 0 additions & 4 deletions docs/Config_Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/Config_checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/Kalico_Additions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 8 additions & 10 deletions docs/MPC.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:`
Expand Down
152 changes: 44 additions & 108 deletions klippy/configfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
#*#
Expand Down Expand Up @@ -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")
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
16 changes: 13 additions & 3 deletions klippy/extras/danger_options.py
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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
)
Expand All @@ -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

Expand All @@ -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
1 change: 0 additions & 1 deletion klippy/extras/telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,6 @@ def _collect_anonymized_config(self):

{
"danger_options": [
"autosave_includes",
"allow_plugin_override",
"log_bed_mesh_at_startup"
],
Expand Down
1 change: 0 additions & 1 deletion test/klippy/danger_options.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
8 changes: 4 additions & 4 deletions test/test_autosave.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
)
1 change: 0 additions & 1 deletion test/test_configs/autosave/danger_options.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading