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:
parent
f80456a698
commit
531134f092
|
@ -115,6 +115,10 @@ The following standard commands are supported:
|
||||||
[the FAQ](FAQ.md#how-do-i-upgrade-to-the-latest-software)).
|
[the FAQ](FAQ.md#how-do-i-upgrade-to-the-latest-software)).
|
||||||
- `FIRMWARE_RESTART`: This is similar to a RESTART command, but it
|
- `FIRMWARE_RESTART`: This is similar to a RESTART command, but it
|
||||||
also clears any error state from the micro-controller.
|
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.
|
- `STATUS`: Report the Klipper host software status.
|
||||||
- `HELP`: Report the list of available extended G-Code commands.
|
- `HELP`: Report the list of available extended G-Code commands.
|
||||||
|
|
||||||
|
|
|
@ -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>
|
# Copyright (C) 2016-2018 Kevin O'Connor <kevin@koconnor.net>
|
||||||
#
|
#
|
||||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
# 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
|
error = ConfigParser.Error
|
||||||
|
|
||||||
|
@ -75,31 +75,104 @@ class ConfigWrapper:
|
||||||
return [self.getsection(s) for s in self.fileconfig.sections()
|
return [self.getsection(s) for s in self.fileconfig.sections()
|
||||||
if s.startswith(prefix)]
|
if s.startswith(prefix)]
|
||||||
|
|
||||||
class ConfigLogger:
|
AUTOSAVE_HEADER = """
|
||||||
def __init__(self, fileconfig, printer):
|
#*# <---------------------- SAVE_CONFIG ---------------------->
|
||||||
self.lines = ["===== Config file ====="]
|
#*# DO NOT EDIT THIS BLOCK OR BELOW. The contents are auto-generated.
|
||||||
fileconfig.write(self)
|
#*#
|
||||||
self.lines.append("=======================")
|
"""
|
||||||
printer.set_rollover_info("config", "\n".join(self.lines))
|
|
||||||
def write(self, data):
|
|
||||||
self.lines.append(data.strip())
|
|
||||||
|
|
||||||
class PrinterConfig:
|
class PrinterConfig:
|
||||||
def __init__(self, printer):
|
def __init__(self, printer):
|
||||||
self.printer = 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()
|
fileconfig = ConfigParser.RawConfigParser()
|
||||||
res = fileconfig.read(filename)
|
fileconfig.readfp(sfile)
|
||||||
if not res:
|
|
||||||
raise error("Unable to open config file %s" % (filename,))
|
|
||||||
return ConfigWrapper(self.printer, fileconfig, {}, 'printer')
|
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):
|
def read_main_config(self):
|
||||||
filename = self.printer.get_start_args()['config_file']
|
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):
|
def check_unused_options(self, config):
|
||||||
access_tracking = config.access_tracking
|
|
||||||
fileconfig = config.fileconfig
|
fileconfig = config.fileconfig
|
||||||
objects = dict(self.printer.lookup_objects())
|
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
|
# Validate that there are no undefined parameters in the config file
|
||||||
valid_sections = { s: 1 for s, o in access_tracking }
|
valid_sections = { s: 1 for s, o in access_tracking }
|
||||||
for section_name in fileconfig.sections():
|
for section_name in fileconfig.sections():
|
||||||
|
@ -113,4 +186,62 @@ class PrinterConfig:
|
||||||
raise error("Option '%s' is not valid in section '%s'" % (
|
raise error("Option '%s' is not valid in section '%s'" % (
|
||||||
option, section))
|
option, section))
|
||||||
def log_config(self, config):
|
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')
|
||||||
|
|
Loading…
Reference in New Issue