| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  | #!/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 | 
					
						
							| 
									
										
										
										
											2016-11-11 20:22:39 -05:00
										 |  |  | import gcode, toolhead, util, mcu, fan, heater, extruder, reactor, queuelogger | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-11-30 14:30:45 -05:00
										 |  |  | message_startup = """
 | 
					
						
							|  |  |  | The klippy host software is attempting to connect.  Please | 
					
						
							|  |  |  | retry in a few moments. | 
					
						
							|  |  |  | Printer is not ready | 
					
						
							|  |  |  | """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | message_restart = """
 | 
					
						
							|  |  |  | This is an unrecoverable error.  Please correct the | 
					
						
							|  |  |  | underlying issue and then manually restart the klippy host | 
					
						
							|  |  |  | software. | 
					
						
							|  |  |  | Printer is halted | 
					
						
							|  |  |  | """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | message_mcu_connect_error = """
 | 
					
						
							|  |  |  | This is an unrecoverable error.  Please manually restart | 
					
						
							|  |  |  | both the firmware and the host software. | 
					
						
							|  |  |  | Error configuring printer | 
					
						
							|  |  |  | """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | message_shutdown = """
 | 
					
						
							|  |  |  | This is an unrecoverable error.  Please correct the | 
					
						
							|  |  |  | underlying issue and then manually restart both the | 
					
						
							|  |  |  | firmware and the host software. | 
					
						
							|  |  |  | Printer is shutdown | 
					
						
							|  |  |  | """
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  | class ConfigWrapper: | 
					
						
							| 
									
										
										
										
											2016-11-30 15:05:26 -05:00
										 |  |  |     error = ConfigParser.Error | 
					
						
							| 
									
										
										
										
											2016-11-30 15:39:36 -05:00
										 |  |  |     class sentinel: | 
					
						
							|  |  |  |         pass | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |     def __init__(self, printer, section): | 
					
						
							|  |  |  |         self.printer = printer | 
					
						
							|  |  |  |         self.section = section | 
					
						
							| 
									
										
										
										
											2016-11-30 15:39:36 -05:00
										 |  |  |     def get_wrapper(self, parser, option, default): | 
					
						
							|  |  |  |         if (default is not self.sentinel | 
					
						
							|  |  |  |             and not self.printer.fileconfig.has_option(self.section, option)): | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |             return default | 
					
						
							| 
									
										
										
										
											2016-11-30 16:07:17 -05:00
										 |  |  |         self.printer.all_config_options[ | 
					
						
							|  |  |  |             (self.section.lower(), option.lower())] = 1 | 
					
						
							| 
									
										
										
										
											2016-11-30 15:39:36 -05:00
										 |  |  |         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) | 
					
						
							| 
									
										
										
										
											2016-11-30 15:50:28 -05:00
										 |  |  |     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] | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |     def getsection(self, section): | 
					
						
							|  |  |  |         return ConfigWrapper(self.printer, section) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class Printer: | 
					
						
							| 
									
										
										
										
											2016-11-20 20:40:31 -05:00
										 |  |  |     def __init__(self, conffile, input_fd, is_fileinput=False): | 
					
						
							| 
									
										
										
										
											2016-11-22 12:14:25 -05:00
										 |  |  |         self.conffile = conffile | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |         self.reactor = reactor.Reactor() | 
					
						
							| 
									
										
										
										
											2016-11-20 20:40:31 -05:00
										 |  |  |         self.gcode = gcode.GCodeParser(self, input_fd, is_fileinput) | 
					
						
							| 
									
										
										
										
											2016-11-22 12:14:25 -05:00
										 |  |  |         self.stats_timer = self.reactor.register_timer(self.stats) | 
					
						
							| 
									
										
										
										
											2016-11-28 13:14:56 -05:00
										 |  |  |         self.connect_timer = self.reactor.register_timer( | 
					
						
							|  |  |  |             self.connect, self.reactor.NOW) | 
					
						
							| 
									
										
										
										
											2016-11-30 16:07:17 -05:00
										 |  |  |         self.all_config_options = {} | 
					
						
							| 
									
										
										
										
											2016-11-30 19:05:25 -05:00
										 |  |  |         self.need_dump_debug = False | 
					
						
							| 
									
										
										
										
											2016-11-30 14:30:45 -05:00
										 |  |  |         self.state_message = message_startup | 
					
						
							| 
									
										
										
										
											2016-11-29 22:46:05 -05:00
										 |  |  |         self.debugoutput = self.dictionary = None | 
					
						
							| 
									
										
										
										
											2016-11-22 12:14:25 -05:00
										 |  |  |         self.fileconfig = None | 
					
						
							|  |  |  |         self.mcu = None | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |         self.objects = {} | 
					
						
							| 
									
										
										
										
											2016-11-22 12:14:25 -05:00
										 |  |  |     def set_fileoutput(self, debugoutput, dictionary): | 
					
						
							|  |  |  |         self.debugoutput = debugoutput | 
					
						
							|  |  |  |         self.dictionary = dictionary | 
					
						
							|  |  |  |     def stats(self, eventtime): | 
					
						
							| 
									
										
										
										
											2016-11-30 19:05:25 -05:00
										 |  |  |         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 | 
					
						
							| 
									
										
										
										
											2016-11-22 12:14:25 -05:00
										 |  |  |         out = [] | 
					
						
							|  |  |  |         out.append(self.gcode.stats(eventtime)) | 
					
						
							|  |  |  |         toolhead = self.objects.get('toolhead') | 
					
						
							|  |  |  |         out.append(toolhead.stats(eventtime)) | 
					
						
							|  |  |  |         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() | 
					
						
							| 
									
										
										
										
											2016-11-30 14:57:18 -05:00
										 |  |  |         res = self.fileconfig.read(self.conffile) | 
					
						
							|  |  |  |         if not res: | 
					
						
							|  |  |  |             raise ConfigParser.Error("Unable to open config file %s" % ( | 
					
						
							|  |  |  |                 self.conffile,)) | 
					
						
							| 
									
										
										
										
											2016-11-22 12:14:25 -05:00
										 |  |  |         self.mcu = mcu.MCU(self, ConfigWrapper(self, 'mcu')) | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |         if self.fileconfig.has_section('fan'): | 
					
						
							|  |  |  |             self.objects['fan'] = fan.PrinterFan( | 
					
						
							|  |  |  |                 self, ConfigWrapper(self, 'fan')) | 
					
						
							| 
									
										
										
										
											2016-07-10 12:23:35 -04:00
										 |  |  |         if self.fileconfig.has_section('extruder'): | 
					
						
							|  |  |  |             self.objects['extruder'] = extruder.PrinterExtruder( | 
					
						
							|  |  |  |                 self, ConfigWrapper(self, 'extruder')) | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |         if self.fileconfig.has_section('heater_bed'): | 
					
						
							|  |  |  |             self.objects['heater_bed'] = heater.PrinterHeater( | 
					
						
							|  |  |  |                 self, ConfigWrapper(self, 'heater_bed')) | 
					
						
							| 
									
										
										
										
											2016-07-07 15:52:44 -04:00
										 |  |  |         self.objects['toolhead'] = toolhead.ToolHead( | 
					
						
							| 
									
										
										
										
											2016-11-20 20:40:31 -05:00
										 |  |  |             self, ConfigWrapper(self, 'printer')) | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |     def build_config(self): | 
					
						
							|  |  |  |         for oname in sorted(self.objects.keys()): | 
					
						
							|  |  |  |             self.objects[oname].build_config() | 
					
						
							|  |  |  |         self.gcode.build_config() | 
					
						
							|  |  |  |         self.mcu.build_config() | 
					
						
							| 
									
										
										
										
											2016-11-30 16:07:17 -05:00
										 |  |  |     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)) | 
					
						
							| 
									
										
										
										
											2016-11-28 13:14:56 -05:00
										 |  |  |     def connect(self, eventtime): | 
					
						
							| 
									
										
										
										
											2016-11-30 14:30:45 -05:00
										 |  |  |         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() | 
					
						
							| 
									
										
										
										
											2016-11-30 16:07:17 -05:00
										 |  |  |             self.validate_config() | 
					
						
							| 
									
										
										
										
											2016-11-30 14:30:45 -05:00
										 |  |  |             self.gcode.set_printer_ready(True) | 
					
						
							|  |  |  |             self.state_message = "Running" | 
					
						
							| 
									
										
										
										
											2016-11-30 14:57:18 -05:00
										 |  |  |         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) | 
					
						
							| 
									
										
										
										
											2016-11-30 14:30:45 -05:00
										 |  |  |         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) | 
					
						
							| 
									
										
										
										
											2016-11-28 13:14:56 -05:00
										 |  |  |         self.reactor.unregister_timer(self.connect_timer) | 
					
						
							|  |  |  |         return self.reactor.NEVER | 
					
						
							|  |  |  |     def run(self): | 
					
						
							| 
									
										
										
										
											2016-11-30 14:30:45 -05:00
										 |  |  |         try: | 
					
						
							|  |  |  |             self.reactor.run() | 
					
						
							|  |  |  |         except: | 
					
						
							|  |  |  |             logging.exception("Unhandled exception during run") | 
					
						
							|  |  |  |             return | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |         # If gcode exits, then exit the MCU | 
					
						
							|  |  |  |         self.stats(time.time()) | 
					
						
							|  |  |  |         self.mcu.disconnect() | 
					
						
							|  |  |  |         self.stats(time.time()) | 
					
						
							| 
									
										
										
										
											2016-11-30 14:30:45 -05:00
										 |  |  |     def get_state_message(self): | 
					
						
							|  |  |  |         return self.state_message | 
					
						
							|  |  |  |     def note_shutdown(self, msg): | 
					
						
							| 
									
										
										
										
											2016-11-30 19:05:25 -05:00
										 |  |  |         if self.state_message == 'Running': | 
					
						
							|  |  |  |             self.need_dump_debug = True | 
					
						
							| 
									
										
										
										
											2016-11-30 14:30:45 -05:00
										 |  |  |         self.state_message = "Firmware shutdown: %s%s" % ( | 
					
						
							|  |  |  |             msg, message_shutdown) | 
					
						
							| 
									
										
										
										
											2016-11-22 19:38:51 -05:00
										 |  |  |         self.gcode.set_printer_ready(False) | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ###################################################################### | 
					
						
							|  |  |  | # 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") | 
					
						
							| 
									
										
										
										
											2016-11-20 20:40:31 -05:00
										 |  |  |     opts.add_option("-I", "--input-tty", dest="inputtty", default='/tmp/printer', | 
					
						
							|  |  |  |                     help="input tty name (default is /tmp/printer)") | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |     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] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-11-20 20:40:31 -05:00
										 |  |  |     input_fd = debuginput = debugoutput = bglogger = None | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |     debuglevel = logging.INFO | 
					
						
							|  |  |  |     if options.verbose: | 
					
						
							|  |  |  |         debuglevel = logging.DEBUG | 
					
						
							|  |  |  |     if options.inputfile: | 
					
						
							|  |  |  |         debuginput = open(options.inputfile, 'rb') | 
					
						
							| 
									
										
										
										
											2016-11-20 20:40:31 -05:00
										 |  |  |         input_fd = debuginput.fileno() | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         input_fd = util.create_pty(options.inputtty) | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |     if options.outputfile: | 
					
						
							|  |  |  |         debugoutput = open(options.outputfile, 'wb') | 
					
						
							|  |  |  |     if options.logfile: | 
					
						
							| 
									
										
										
										
											2016-11-11 20:22:39 -05:00
										 |  |  |         bglogger = queuelogger.setup_bg_logging(options.logfile, debuglevel) | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |     else: | 
					
						
							|  |  |  |         logging.basicConfig(level=debuglevel) | 
					
						
							|  |  |  |     logging.info("Starting Klippy...") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Start firmware | 
					
						
							| 
									
										
										
										
											2016-11-20 20:40:31 -05:00
										 |  |  |     printer = Printer(conffile, input_fd, is_fileinput=debuginput is not None) | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |     if debugoutput: | 
					
						
							|  |  |  |         proto_dict = read_dictionary(options.read_dictionary) | 
					
						
							| 
									
										
										
										
											2016-11-29 22:46:05 -05:00
										 |  |  |         printer.set_fileoutput(debugoutput, proto_dict) | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |     printer.run() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-11-11 20:22:39 -05:00
										 |  |  |     if bglogger is not None: | 
					
						
							|  |  |  |         bglogger.stop() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  | if __name__ == '__main__': | 
					
						
							|  |  |  |     main() |