From d3665699f168bc1b1413fa8dddba318d6adbdf87 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 19 Jan 2018 22:49:27 -0500 Subject: [PATCH] klippy: Support generic printer_state() and stats() callbacks Instead of hardcoding which objects are called on state transitions, allow any "printer object" to be invoked if it has a printer_state() method. Convert connect, ready, shutdown, and disconnect callbacks to this mechanism. Similarly, allow all printer objects to provide a stats() callback. Signed-off-by: Kevin O'Connor --- klippy/gcode.py | 21 +++++++++-------- klippy/klippy.py | 57 ++++++++++++++++++++++++---------------------- klippy/mcu.py | 37 ++++++++++++++++++------------ klippy/toolhead.py | 13 ++++++----- 4 files changed, 71 insertions(+), 57 deletions(-) diff --git a/klippy/gcode.py b/klippy/gcode.py index 261bbf22..c6d5e23d 100644 --- a/klippy/gcode.py +++ b/klippy/gcode.py @@ -68,7 +68,18 @@ class GCodeParser: self.gcode_help[cmd] = desc def stats(self, eventtime): return "gcodein=%d" % (self.bytes_read,) - def connect(self): + def printer_state(self, state): + if state == 'shutdown': + if not self.is_printer_ready: + return + self.is_printer_ready = False + self.gcode_handlers = self.base_gcode_handlers + self.dump_debug() + if self.is_fileinput: + self.printer.request_exit() + return + if state != 'ready': + return self.is_printer_ready = True self.gcode_handlers = self.ready_gcode_handlers # Lookup printer components @@ -85,14 +96,6 @@ class GCodeParser: def reset_last_position(self): if self.toolhead is not None: self.last_position = self.toolhead.get_position() - def do_shutdown(self): - if not self.is_printer_ready: - return - self.is_printer_ready = False - self.gcode_handlers = self.base_gcode_handlers - self.dump_debug() - if self.is_fileinput: - self.printer.request_exit() def motor_heater_off(self): if self.toolhead is None: return diff --git a/klippy/klippy.py b/klippy/klippy.py index b21eb56c..da891717 100644 --- a/klippy/klippy.py +++ b/klippy/klippy.py @@ -4,9 +4,10 @@ # Copyright (C) 2016-2018 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. -import sys, optparse, ConfigParser, logging, time, threading -import util, reactor, queuelogger, msgproto, gcode -import pins, mcu, chipmisc, toolhead, extruder, heater, fan +import sys, optparse, logging, time, threading +import collections, ConfigParser, importlib +import util, reactor, queuelogger, msgproto +import gcode, pins, mcu, chipmisc, toolhead, extruder, heater, fan message_ready = "Printer is ready" @@ -134,8 +135,8 @@ class Printer: if bglogger is not None: bglogger.set_rollover_info("config", None) self.reactor = reactor.Reactor() - self.gcode = gcode.GCodeParser(self, input_fd) - self.objects = {'gcode': self.gcode} + gc = gcode.GCodeParser(self, input_fd) + self.objects = collections.OrderedDict({'gcode': gc}) self.stats_timer = self.reactor.register_timer(self._stats) self.connect_timer = self.reactor.register_timer( self._connect, self.reactor.NOW) @@ -145,7 +146,8 @@ class Printer: self.async_shutdown_msg = "" self.run_result = None self.fileconfig = None - self.mcus = [] + self.stats_cb = [] + self.state_cb = [] def get_start_args(self): return self.start_args def get_reactor(self): @@ -165,8 +167,7 @@ class Printer: return default def lookup_module_objects(self, module_name): prefix = module_name + ' ' - objs = [self.objects[n] - for n in sorted(self.objects) if n.startswith(prefix)] + objs = [self.objects[n] for n in self.objects if n.startswith(prefix)] if module_name in self.objects: return [self.objects[module_name]] + objs return objs @@ -180,12 +181,8 @@ class Printer: is_active = toolhead.check_active(eventtime) if not is_active and not force_output: return eventtime + 1. - out = [] - out.append(self.gcode.stats(eventtime)) - out.append(toolhead.stats(eventtime)) - for m in self.mcus: - out.append(m.stats(eventtime)) - logging.info("Stats %.1f: %s", eventtime, ' '.join(out)) + stats = [cb(eventtime) for cb in self.stats_cb] + logging.info("Stats %.1f: %s", eventtime, ' '.join(stats)) return eventtime + 1. def _load_config(self): self.fileconfig = ConfigParser.RawConfigParser() @@ -200,7 +197,6 @@ class Printer: config = ConfigWrapper(self, 'printer') for m in [pins, mcu, chipmisc, toolhead, extruder, heater, fan]: m.add_printer_objects(self, config) - self.mcus = self.lookup_module_objects('mcu') # Validate that there are no undefined parameters in the config file valid_sections = { s: 1 for s, o in self.all_config_options } for section in self.fileconfig.sections(): @@ -214,14 +210,24 @@ class Printer: raise self.config_error( "Unknown option '%s' in section '%s'" % ( option, section)) + # Determine which printer objects have stats/state callbacks + self.stats_cb = [o.stats for o in self.objects.values() + if hasattr(o, 'stats')] + self.state_cb = [o.printer_state for o in self.objects.values() + if hasattr(o, 'printer_state')] def _connect(self, eventtime): self.reactor.unregister_timer(self.connect_timer) try: self._load_config() - for m in self.mcus: - m.connect() - self.gcode.connect() + for cb in self.state_cb: + if self.state_message is not message_startup: + return self.reactor.NEVER + cb('connect') self.state_message = message_ready + for cb in self.state_cb: + if self.state_message is not message_ready: + return self.reactor.NEVER + cb('ready') if self.start_args.get('debugoutput') is None: self.reactor.update_timer(self.stats_timer, self.reactor.NOW) except (self.config_error, pins.error) as e: @@ -257,10 +263,11 @@ class Printer: self.invoke_shutdown(self.async_shutdown_msg) continue self._stats(self.reactor.monotonic(), force_output=True) - for m in self.mcus: - if run_result == 'firmware_restart': + if run_result == 'firmware_restart': + for m in self.lookup_module_objects('mcu'): m.microcontroller_restart() - m.disconnect() + for cb in self.state_cb: + cb('disconnect') except: logging.exception("Unhandled exception during post run") return run_result @@ -269,12 +276,8 @@ class Printer: return self.is_shutdown = True self.state_message = "%s%s" % (msg, message_shutdown) - for m in self.mcus: - m.do_shutdown() - self.gcode.do_shutdown() - toolhead = self.objects.get('toolhead') - if toolhead is not None: - toolhead.do_shutdown() + for cb in self.state_cb: + cb('shutdown') def invoke_async_shutdown(self, msg): self.async_shutdown_msg = msg self.request_exit("shutdown") diff --git a/klippy/mcu.py b/klippy/mcu.py index 15f9fcea..c4f9d969 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -581,7 +581,7 @@ class MCU: self._ffi_lib.steppersync_set_time(self._steppersync, 0., self._mcu_freq) for c in self._init_cmds: self.send(self.create_command(c)) - def connect(self): + def _connect(self): if self.is_fileoutput(): self._connect_file() else: @@ -666,9 +666,18 @@ class MCU: def monotonic(self): return self._reactor.monotonic() # Restarts + def _disconnect(self): + self._serial.disconnect() + if self._steppersync is not None: + self._ffi_lib.steppersync_free(self._steppersync) + self._steppersync = None + def _shutdown(self, force=False): + if self._emergency_stop_cmd is None or (self._is_shutdown and not force): + return + self.send(self._emergency_stop_cmd.encode()) def _restart_arduino(self): logging.info("Attempting MCU '%s' reset", self._name) - self.disconnect() + self._disconnect() serialhdl.arduino_reset(self._serialport, self._reactor) def _restart_via_command(self): if ((self._reset_cmd is None and self._config_reset_cmd is None) @@ -679,7 +688,7 @@ class MCU: # Attempt reset via config_reset command logging.info("Attempting MCU '%s' config_reset command", self._name) self._is_shutdown = True - self.do_shutdown(force=True) + self._shutdown(force=True) self._reactor.pause(self._reactor.monotonic() + 0.015) self.send(self._config_reset_cmd.encode()) else: @@ -687,10 +696,10 @@ class MCU: logging.info("Attempting MCU '%s' reset command", self._name) self.send(self._reset_cmd.encode()) self._reactor.pause(self._reactor.monotonic() + 0.015) - self.disconnect() + self._disconnect() def _restart_rpi_usb(self): logging.info("Attempting MCU '%s' reset via rpi usb power", self._name) - self.disconnect() + self._disconnect() chelper.run_hub_ctrl(0) self._reactor.pause(self._reactor.monotonic() + 2.) chelper.run_hub_ctrl(1) @@ -735,17 +744,15 @@ class MCU: self._mcu_tick_stddev) return ' '.join([msg, self._serial.stats(eventtime), self._clocksync.stats(eventtime)]) - def do_shutdown(self, force=False): - if self._emergency_stop_cmd is None or (self._is_shutdown and not force): - return - self.send(self._emergency_stop_cmd.encode()) - def disconnect(self): - self._serial.disconnect() - if self._steppersync is not None: - self._ffi_lib.steppersync_free(self._steppersync) - self._steppersync = None + def printer_state(self, state): + if state == 'connect': + self._connect() + elif state == 'disconnect': + self._disconnect() + elif state == 'shutdown': + self._shutdown() def __del__(self): - self.disconnect() + self._disconnect() Common_MCU_errors = { ("Timer too close", "No next step", "Missed scheduling of next "): """ diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 83357732..19d8b689 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -369,12 +369,13 @@ class ToolHead: buffer_time = max(0., self.print_time - est_print_time) return "print_time=%.3f buffer_time=%.3f print_stall=%d" % ( self.print_time, buffer_time, self.print_stall) - def do_shutdown(self): - try: - self.move_queue.reset() - self.reset_print_time() - except: - logging.exception("Exception in do_shutdown") + def printer_state(self, state): + if state == 'shutdown': + try: + self.move_queue.reset() + self.reset_print_time() + except: + logging.exception("Exception in toolhead shutdown") def get_kinematics(self): return self.kin def get_max_velocity(self):