klipper/klippy/klippy.py

288 lines
11 KiB
Python

#!/usr/bin/env python
# Main code for host side printer firmware
#
# Copyright (C) 2016 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import sys, optparse, ConfigParser, logging, time, threading
import gcode, toolhead, util, mcu, fan, heater, extruder, reactor, queuelogger
message_startup = """
The klippy host software is attempting to connect. Please
retry in a few moments.
Printer is not ready
"""
message_restart = """
Once the underlying issue is corrected, use the "RESTART"
command to reload the config and restart the host software.
Printer is halted
"""
message_mcu_connect_error = """
This is an unrecoverable error. Please manually restart the
micro-controller and then issue the "RESTART" command to
restart the host software.
Error configuring printer
"""
message_shutdown = """
Once the underlying issue is corrected, the "CLEAR_SHUTDOWN"
command can be used to clear the firmware flag and restart
the host software.
Printer is shutdown
"""
class ConfigWrapper:
error = ConfigParser.Error
class sentinel:
pass
def __init__(self, printer, section):
self.printer = printer
self.section = section
def get_wrapper(self, parser, option, default):
if (default is not self.sentinel
and not self.printer.fileconfig.has_option(self.section, option)):
return default
self.printer.all_config_options[
(self.section.lower(), option.lower())] = 1
try:
return parser(self.section, option)
except self.error, e:
raise
except:
raise self.error("Unable to parse option '%s' in section '%s'" % (
option, self.section))
def get(self, option, default=sentinel):
return self.get_wrapper(self.printer.fileconfig.get, option, default)
def getint(self, option, default=sentinel):
return self.get_wrapper(self.printer.fileconfig.getint, option, default)
def getfloat(self, option, default=sentinel):
return self.get_wrapper(
self.printer.fileconfig.getfloat, option, default)
def getboolean(self, option, default=sentinel):
return self.get_wrapper(
self.printer.fileconfig.getboolean, option, default)
def getchoice(self, option, choices, default=sentinel):
c = self.get(option, default)
if c not in choices:
raise self.error(
"Option '%s' in section '%s' is not a valid choice" % (
option, self.section))
return choices[c]
def getsection(self, section):
return ConfigWrapper(self.printer, section)
class ConfigLogger():
def __init__(self, cfg):
logging.info("===== Config file =====")
cfg.write(self)
logging.info("=======================")
def write(self, data):
logging.info(data.strip())
class Printer:
def __init__(self, conffile, input_fd, is_fileinput=False):
self.conffile = conffile
self.reactor = reactor.Reactor()
self.gcode = gcode.GCodeParser(self, input_fd, is_fileinput)
self.stats_timer = self.reactor.register_timer(self.stats)
self.connect_timer = self.reactor.register_timer(
self.connect, self.reactor.NOW)
self.all_config_options = {}
self.need_dump_debug = False
self.state_message = message_startup
self.debugoutput = self.dictionary = None
self.run_result = None
self.fileconfig = None
self.mcu = None
self.objects = {}
def set_fileoutput(self, debugoutput, dictionary):
self.debugoutput = debugoutput
self.dictionary = dictionary
def stats(self, eventtime):
if self.need_dump_debug:
# Call dump_debug here so it is executed in the main thread
self.gcode.dump_debug()
self.need_dump_debug = False
out = []
out.append(self.gcode.stats(eventtime))
toolhead = self.objects.get('toolhead')
if toolhead is not None:
out.append(toolhead.stats(eventtime))
if self.mcu is not None:
out.append(self.mcu.stats(eventtime))
logging.info("Stats %.0f: %s" % (eventtime, ' '.join(out)))
return eventtime + 1.
def load_config(self):
self.fileconfig = ConfigParser.RawConfigParser()
res = self.fileconfig.read(self.conffile)
if not res:
raise ConfigParser.Error("Unable to open config file %s" % (
self.conffile,))
if self.debugoutput is None:
ConfigLogger(self.fileconfig)
self.mcu = mcu.MCU(self, ConfigWrapper(self, 'mcu'))
if self.fileconfig.has_section('fan'):
self.objects['fan'] = fan.PrinterFan(
self, ConfigWrapper(self, 'fan'))
if self.fileconfig.has_section('extruder'):
self.objects['extruder'] = extruder.PrinterExtruder(
self, ConfigWrapper(self, 'extruder'))
if self.fileconfig.has_section('heater_bed'):
self.objects['heater_bed'] = heater.PrinterHeater(
self, ConfigWrapper(self, 'heater_bed'))
self.objects['toolhead'] = toolhead.ToolHead(
self, ConfigWrapper(self, 'printer'))
def build_config(self):
for oname in sorted(self.objects.keys()):
self.objects[oname].build_config()
self.gcode.build_config()
self.mcu.build_config()
def validate_config(self):
valid_sections = dict([(s, 1) for s, o in self.all_config_options])
for section in self.fileconfig.sections():
section = section.lower()
if section not in valid_sections:
raise ConfigParser.Error("Unknown config file section '%s'" % (
section,))
for option in self.fileconfig.options(section):
option = option.lower()
if (section, option) not in self.all_config_options:
raise ConfigParser.Error(
"Unknown option '%s' in section '%s'" % (
option, section))
def connect(self, eventtime):
try:
self.load_config()
if self.debugoutput is None:
self.reactor.update_timer(self.stats_timer, self.reactor.NOW)
else:
self.mcu.connect_file(self.debugoutput, self.dictionary)
self.mcu.connect()
self.build_config()
self.validate_config()
self.gcode.set_printer_ready(True)
self.state_message = "Printer is ready"
except ConfigParser.Error, e:
logging.exception("Config error")
self.state_message = "%s%s" % (str(e), message_restart)
self.reactor.update_timer(self.stats_timer, self.reactor.NEVER)
except mcu.error, e:
logging.exception("MCU error during connect")
self.state_message = "%s%s" % (str(e), message_mcu_connect_error)
self.reactor.update_timer(self.stats_timer, self.reactor.NEVER)
except:
logging.exception("Unhandled exception during connect")
self.state_message = "Internal error during connect.%s" % (
message_restart)
self.reactor.update_timer(self.stats_timer, self.reactor.NEVER)
self.reactor.unregister_timer(self.connect_timer)
return self.reactor.NEVER
def run(self):
try:
self.reactor.run()
except:
logging.exception("Unhandled exception during run")
return
return self.run_result
def get_state_message(self):
return self.state_message
def note_shutdown(self, msg):
if self.state_message == 'Running':
self.need_dump_debug = True
self.state_message = "Firmware shutdown: %s%s" % (
msg, message_shutdown)
self.gcode.set_printer_ready(False)
def note_mcu_error(self, msg):
self.state_message = "%s%s" % (msg, message_restart)
self.gcode.set_printer_ready(False)
self.gcode.motor_heater_off()
def disconnect(self):
try:
if self.mcu is not None:
self.stats(time.time())
self.mcu.disconnect()
except:
logging.exception("Unhandled exception during disconnect")
def request_restart(self):
self.run_result = "restart"
self.reactor.end()
def request_exit_eof(self):
self.run_result = "exit_eof"
self.reactor.end()
######################################################################
# Startup
######################################################################
def read_dictionary(filename):
dfile = open(filename, 'rb')
dictionary = dfile.read()
dfile.close()
return dictionary
def main():
usage = "%prog [options] <config file>"
opts = optparse.OptionParser(usage)
opts.add_option("-o", "--debugoutput", dest="outputfile",
help="write output to file instead of to serial port")
opts.add_option("-i", "--debuginput", dest="inputfile",
help="read commands from file instead of from tty port")
opts.add_option("-I", "--input-tty", dest="inputtty", default='/tmp/printer',
help="input tty name (default is /tmp/printer)")
opts.add_option("-l", "--logfile", dest="logfile",
help="write log to file instead of stderr")
opts.add_option("-v", action="store_true", dest="verbose",
help="enable debug messages")
opts.add_option("-d", dest="read_dictionary",
help="file to read for mcu protocol dictionary")
options, args = opts.parse_args()
if len(args) != 1:
opts.error("Incorrect number of arguments")
conffile = args[0]
input_fd = debuginput = debugoutput = bglogger = None
debuglevel = logging.INFO
if options.verbose:
debuglevel = logging.DEBUG
if options.inputfile:
debuginput = open(options.inputfile, 'rb')
input_fd = debuginput.fileno()
else:
input_fd = util.create_pty(options.inputtty)
if options.outputfile:
debugoutput = open(options.outputfile, 'wb')
if options.logfile:
bglogger = queuelogger.setup_bg_logging(options.logfile, debuglevel)
else:
logging.basicConfig(level=debuglevel)
logging.info("Starting Klippy...")
if debugoutput is None:
util.report_git_version()
# Start firmware
while 1:
is_fileinput = debuginput is not None
printer = Printer(conffile, input_fd, is_fileinput)
if debugoutput:
proto_dict = read_dictionary(options.read_dictionary)
printer.set_fileoutput(debugoutput, proto_dict)
res = printer.run()
if res == 'restart':
printer.disconnect()
time.sleep(1.)
logging.info("Restarting printer")
continue
elif res == 'exit_eof':
printer.disconnect()
break
if bglogger is not None:
bglogger.stop()
if __name__ == '__main__':
main()