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>
This commit is contained in:
Kevin O'Connor 2018-09-16 11:37:00 -04:00 committed by KevinOConnor
parent f80456a698
commit 531134f092
2 changed files with 152 additions and 17 deletions

View file

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