aboutsummaryrefslogtreecommitdiffstats
path: root/klippy/configfile.py
diff options
context:
space:
mode:
authorKevin O'Connor <kevin@koconnor.net>2018-09-16 11:37:00 -0400
committerKevinOConnor <kevin@koconnor.net>2018-09-25 13:48:46 -0400
commit531134f092555f737e7031c1b011fb85cb051f71 (patch)
tree33fc17ede64b93c76a1980524812e5e2cf87a40a /klippy/configfile.py
parentf80456a6983566ae073e477b9033090263c4745d (diff)
downloadkutter-531134f092555f737e7031c1b011fb85cb051f71.tar.gz
kutter-531134f092555f737e7031c1b011fb85cb051f71.tar.xz
kutter-531134f092555f737e7031c1b011fb85cb051f71.zip
configfile: Add support for rewriting the printer config file
Add support for writing back the main printer config file with additional calibration data stored in it. Signed-off-by: Kevin O'Connor <kevin@koconnor.net>
Diffstat (limited to 'klippy/configfile.py')
-rw-r--r--klippy/configfile.py165
1 files changed, 148 insertions, 17 deletions
diff --git a/klippy/configfile.py b/klippy/configfile.py
index 9c4ad4e1..b8945462 100644
--- a/klippy/configfile.py
+++ b/klippy/configfile.py
@@ -1,9 +1,9 @@
-# Code for reading the Klipper config file
+# Code for reading and writing the Klipper config file
#
# Copyright (C) 2016-2018 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
-import ConfigParser
+import os, re, time, logging, ConfigParser, StringIO
error = ConfigParser.Error
@@ -75,31 +75,104 @@ class ConfigWrapper:
return [self.getsection(s) for s in self.fileconfig.sections()
if s.startswith(prefix)]
-class ConfigLogger:
- def __init__(self, fileconfig, printer):
- self.lines = ["===== Config file ====="]
- fileconfig.write(self)
- self.lines.append("=======================")
- printer.set_rollover_info("config", "\n".join(self.lines))
- def write(self, data):
- self.lines.append(data.strip())
+AUTOSAVE_HEADER = """
+#*# <---------------------- SAVE_CONFIG ---------------------->
+#*# DO NOT EDIT THIS BLOCK OR BELOW. The contents are auto-generated.
+#*#
+"""
class PrinterConfig:
def __init__(self, printer):
self.printer = printer
- def read_config(self, filename):
+ self.autosave = None
+ gcode = self.printer.lookup_object('gcode')
+ gcode.register_command("SAVE_CONFIG", self.cmd_SAVE_CONFIG,
+ desc=self.cmd_SAVE_CONFIG_help)
+ def _read_config_file(self, filename):
+ try:
+ f = open(filename, 'rb')
+ data = f.read()
+ f.close()
+ except:
+ msg = "Unable to open config file %s" % (filename,)
+ logging.exception(msg)
+ raise error(msg)
+ return data.replace('\r\n', '\n')
+ def _find_autosave_data(self, data):
+ regular_data = data
+ autosave_data = ""
+ pos = data.find(AUTOSAVE_HEADER)
+ if pos >= 0:
+ regular_data = data[:pos]
+ autosave_data = data[pos + len(AUTOSAVE_HEADER):].strip()
+ # Check for errors and strip line prefixes
+ if "\n#*# " in regular_data:
+ logging.warn("Can't read autosave from config file"
+ " - autosave state corrupted")
+ return data, ""
+ out = [""]
+ for line in autosave_data.split('\n'):
+ if ((not line.startswith("#*#")
+ or (len(line) >= 4 and not line.startswith("#*# ")))
+ and autosave_data):
+ logging.warn("Can't read autosave from config file"
+ " - modifications after header")
+ return data, ""
+ out.append(line[4:])
+ out.append("")
+ return regular_data, "\n".join(out)
+ comment_r = re.compile('[#;].*$')
+ value_r = re.compile('[^A-Za-z0-9_].*$')
+ def _strip_duplicates(self, data, config):
+ fileconfig = config.fileconfig
+ # Comment out fields in 'data' that are defined in 'config'
+ lines = data.split('\n')
+ section = None
+ is_dup_field = False
+ for lineno, line in enumerate(lines):
+ pruned_line = self.comment_r.sub('', line).rstrip()
+ if not pruned_line:
+ continue
+ if pruned_line[0].isspace():
+ if is_dup_field:
+ lines[lineno] = '#' + lines[lineno]
+ continue
+ is_dup_field = False
+ if pruned_line[0] == '[':
+ section = pruned_line[1:-1].strip()
+ continue
+ field = self.value_r.sub('', pruned_line)
+ if config.fileconfig.has_option(section, field):
+ is_dup_field = True
+ lines[lineno] = '#' + lines[lineno]
+ return "\n".join(lines)
+ def _build_config_wrapper(self, data):
+ sfile = StringIO.StringIO(data)
fileconfig = ConfigParser.RawConfigParser()
- res = fileconfig.read(filename)
- if not res:
- raise error("Unable to open config file %s" % (filename,))
+ fileconfig.readfp(sfile)
return ConfigWrapper(self.printer, fileconfig, {}, 'printer')
+ def _build_config_string(self, config):
+ sfile = StringIO.StringIO()
+ config.fileconfig.write(sfile)
+ return sfile.getvalue().strip()
+ def read_config(self, filename):
+ return self._build_config_wrapper(self._read_config_file(filename))
def read_main_config(self):
filename = self.printer.get_start_args()['config_file']
- return self.read_config(filename)
+ data = self._read_config_file(filename)
+ regular_data, autosave_data = self._find_autosave_data(data)
+ regular_config = self._build_config_wrapper(regular_data)
+ autosave_data = self._strip_duplicates(autosave_data, regular_config)
+ self.autosave = self._build_config_wrapper(autosave_data)
+ return self._build_config_wrapper(regular_data + autosave_data)
def check_unused_options(self, config):
- access_tracking = config.access_tracking
fileconfig = config.fileconfig
objects = dict(self.printer.lookup_objects())
+ # Determine all the fields that have been accessed
+ access_tracking = dict(config.access_tracking)
+ for section in self.autosave.fileconfig.sections():
+ for option in self.autosave.fileconfig.options(section):
+ access_tracking[(section.lower(), option.lower())] = 1
# Validate that there are no undefined parameters in the config file
valid_sections = { s: 1 for s, o in access_tracking }
for section_name in fileconfig.sections():
@@ -113,4 +186,62 @@ class PrinterConfig:
raise error("Option '%s' is not valid in section '%s'" % (
option, section))
def log_config(self, config):
- ConfigLogger(config.fileconfig, self.printer)
+ lines = ["===== Config file =====",
+ self._build_config_string(config),
+ "======================="]
+ self.printer.set_rollover_info("config", "\n".join(lines))
+ # Autosave functions
+ def set(self, section, option, value):
+ if not self.autosave.fileconfig.has_section(section):
+ self.autosave.fileconfig.add_section(section)
+ svalue = str(value)
+ self.autosave.fileconfig.set(section, option, svalue)
+ logging.info("save_config: set [%s] %s = %s", section, option, svalue)
+ def remove_section(self, section):
+ self.autosave.fileconfig.remove_section(section)
+ cmd_SAVE_CONFIG_help = "Overwrite config file and restart"
+ def cmd_SAVE_CONFIG(self, params):
+ 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)
+ # Read in and validate current config file
+ cfgname = self.printer.get_start_args()['config_file']
+ try:
+ data = self._read_config_file(cfgname)
+ regular_data, old_autosave_data = self._find_autosave_data(data)
+ config = self._build_config_wrapper(regular_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)
+ data = regular_data.rstrip() + autosave_data
+ # Determine filenames
+ datestr = time.strftime("-%Y%m%d_%H%M%S")
+ backup_name = cfgname + datestr
+ temp_name = cfgname + "_autosave"
+ if cfgname.endswith(".cfg"):
+ backup_name = cfgname[:-4] + datestr + ".cfg"
+ temp_name = cfgname[:-4] + "_autosave.cfg"
+ # Create new config file with temporary name and swap with main config
+ logging.info("SAVE_CONFIG to '%s' (backup in '%s')",
+ cfgname, backup_name)
+ try:
+ f = open(temp_name, 'wb')
+ f.write(data)
+ f.close()
+ os.rename(cfgname, backup_name)
+ os.rename(temp_name, cfgname)
+ except:
+ msg = "Unable to write config file during SAVE_CONFIG"
+ logging.exception(msg)
+ raise gcode.error(msg)
+ # Request a restart
+ gcode.request_restart('restart')