mirror of
https://github.com/Klipper3d/klipper.git
synced 2025-07-16 03:07:56 -06:00

The normal system clock can have sudden jumps if the system clock is changed. Use the system monotonic clock to avoid these sudden changes in time. It appears the Raspbian OS (which is used by OctoPi) is setup to update the system clock upon network connectivity. This could cause sudden system clock changes which could lead to Klippy processing errors. Using the monotonic clock eliminates these issues. Signed-off-by: Kevin O'Connor <kevin@koconnor.net>
307 lines
12 KiB
Python
307 lines
12 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
|
|
import msgproto
|
|
|
|
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_protocol_error = """
|
|
This type of error is frequently caused by running an older
|
|
version of the firmware on the micro-controller (fix by
|
|
recompiling and flashing the firmware).
|
|
Once the underlying issue is corrected, use the "RESTART"
|
|
command to reload the config and restart the host software.
|
|
Protocol error connecting to printer
|
|
"""
|
|
|
|
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, version="?"):
|
|
self.conffile = conffile
|
|
self.software_version = version
|
|
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 %.1f: %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 msgproto.error, e:
|
|
logging.exception("Protocol error")
|
|
self.state_message = "%s%s" % (str(e), message_protocol_error)
|
|
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):
|
|
systime = time.time()
|
|
monotime = self.reactor.monotonic()
|
|
logging.info("Start printer at %s (%.1f %.1f)" % (
|
|
time.asctime(time.localtime(systime)), systime, monotime))
|
|
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(self.reactor.monotonic())
|
|
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...")
|
|
software_version = util.get_git_version()
|
|
if debugoutput is None:
|
|
logging.info("Git version: %s" % (repr(software_version),))
|
|
|
|
# Start firmware
|
|
while 1:
|
|
is_fileinput = debuginput is not None
|
|
printer = Printer(conffile, input_fd, is_fileinput, software_version)
|
|
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()
|