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

@ -115,6 +115,10 @@ The following standard commands are supported:
[the FAQ](FAQ.md#how-do-i-upgrade-to-the-latest-software)).
- `FIRMWARE_RESTART`: This is similar to a RESTART command, but it
also clears any error state from the micro-controller.
- `SAVE_CONFIG`: This command will overwrite the main printer config
file and restart the host software. This command is used in
conjunction with other calibration commands to store the results of
calibration tests.
- `STATUS`: Report the Klipper host software status.
- `HELP`: Report the list of available extended G-Code commands.

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