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 <kevin@koconnor.net>
This commit is contained in:
Kevin O'Connor 2018-01-19 22:49:27 -05:00
parent 81013ba5c8
commit d3665699f1
4 changed files with 71 additions and 57 deletions

View File

@ -68,7 +68,18 @@ class GCodeParser:
self.gcode_help[cmd] = desc self.gcode_help[cmd] = desc
def stats(self, eventtime): def stats(self, eventtime):
return "gcodein=%d" % (self.bytes_read,) 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.is_printer_ready = True
self.gcode_handlers = self.ready_gcode_handlers self.gcode_handlers = self.ready_gcode_handlers
# Lookup printer components # Lookup printer components
@ -85,14 +96,6 @@ class GCodeParser:
def reset_last_position(self): def reset_last_position(self):
if self.toolhead is not None: if self.toolhead is not None:
self.last_position = self.toolhead.get_position() 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): def motor_heater_off(self):
if self.toolhead is None: if self.toolhead is None:
return return

View File

@ -4,9 +4,10 @@
# 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 sys, optparse, ConfigParser, logging, time, threading import sys, optparse, logging, time, threading
import util, reactor, queuelogger, msgproto, gcode import collections, ConfigParser, importlib
import pins, mcu, chipmisc, toolhead, extruder, heater, fan import util, reactor, queuelogger, msgproto
import gcode, pins, mcu, chipmisc, toolhead, extruder, heater, fan
message_ready = "Printer is ready" message_ready = "Printer is ready"
@ -134,8 +135,8 @@ class Printer:
if bglogger is not None: if bglogger is not None:
bglogger.set_rollover_info("config", None) bglogger.set_rollover_info("config", None)
self.reactor = reactor.Reactor() self.reactor = reactor.Reactor()
self.gcode = gcode.GCodeParser(self, input_fd) gc = gcode.GCodeParser(self, input_fd)
self.objects = {'gcode': self.gcode} self.objects = collections.OrderedDict({'gcode': gc})
self.stats_timer = self.reactor.register_timer(self._stats) self.stats_timer = self.reactor.register_timer(self._stats)
self.connect_timer = self.reactor.register_timer( self.connect_timer = self.reactor.register_timer(
self._connect, self.reactor.NOW) self._connect, self.reactor.NOW)
@ -145,7 +146,8 @@ class Printer:
self.async_shutdown_msg = "" self.async_shutdown_msg = ""
self.run_result = None self.run_result = None
self.fileconfig = None self.fileconfig = None
self.mcus = [] self.stats_cb = []
self.state_cb = []
def get_start_args(self): def get_start_args(self):
return self.start_args return self.start_args
def get_reactor(self): def get_reactor(self):
@ -165,8 +167,7 @@ class Printer:
return default return default
def lookup_module_objects(self, module_name): def lookup_module_objects(self, module_name):
prefix = module_name + ' ' prefix = module_name + ' '
objs = [self.objects[n] objs = [self.objects[n] for n in self.objects if n.startswith(prefix)]
for n in sorted(self.objects) if n.startswith(prefix)]
if module_name in self.objects: if module_name in self.objects:
return [self.objects[module_name]] + objs return [self.objects[module_name]] + objs
return objs return objs
@ -180,12 +181,8 @@ class Printer:
is_active = toolhead.check_active(eventtime) is_active = toolhead.check_active(eventtime)
if not is_active and not force_output: if not is_active and not force_output:
return eventtime + 1. return eventtime + 1.
out = [] stats = [cb(eventtime) for cb in self.stats_cb]
out.append(self.gcode.stats(eventtime)) logging.info("Stats %.1f: %s", eventtime, ' '.join(stats))
out.append(toolhead.stats(eventtime))
for m in self.mcus:
out.append(m.stats(eventtime))
logging.info("Stats %.1f: %s", eventtime, ' '.join(out))
return eventtime + 1. return eventtime + 1.
def _load_config(self): def _load_config(self):
self.fileconfig = ConfigParser.RawConfigParser() self.fileconfig = ConfigParser.RawConfigParser()
@ -200,7 +197,6 @@ class Printer:
config = ConfigWrapper(self, 'printer') config = ConfigWrapper(self, 'printer')
for m in [pins, mcu, chipmisc, toolhead, extruder, heater, fan]: for m in [pins, mcu, chipmisc, toolhead, extruder, heater, fan]:
m.add_printer_objects(self, config) m.add_printer_objects(self, config)
self.mcus = self.lookup_module_objects('mcu')
# 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 self.all_config_options } valid_sections = { s: 1 for s, o in self.all_config_options }
for section in self.fileconfig.sections(): for section in self.fileconfig.sections():
@ -214,14 +210,24 @@ class Printer:
raise self.config_error( raise self.config_error(
"Unknown option '%s' in section '%s'" % ( "Unknown option '%s' in section '%s'" % (
option, section)) 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): def _connect(self, eventtime):
self.reactor.unregister_timer(self.connect_timer) self.reactor.unregister_timer(self.connect_timer)
try: try:
self._load_config() self._load_config()
for m in self.mcus: for cb in self.state_cb:
m.connect() if self.state_message is not message_startup:
self.gcode.connect() return self.reactor.NEVER
cb('connect')
self.state_message = message_ready 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: if self.start_args.get('debugoutput') is None:
self.reactor.update_timer(self.stats_timer, self.reactor.NOW) self.reactor.update_timer(self.stats_timer, self.reactor.NOW)
except (self.config_error, pins.error) as e: except (self.config_error, pins.error) as e:
@ -257,10 +263,11 @@ class Printer:
self.invoke_shutdown(self.async_shutdown_msg) self.invoke_shutdown(self.async_shutdown_msg)
continue continue
self._stats(self.reactor.monotonic(), force_output=True) 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.microcontroller_restart()
m.disconnect() for cb in self.state_cb:
cb('disconnect')
except: except:
logging.exception("Unhandled exception during post run") logging.exception("Unhandled exception during post run")
return run_result return run_result
@ -269,12 +276,8 @@ class Printer:
return return
self.is_shutdown = True self.is_shutdown = True
self.state_message = "%s%s" % (msg, message_shutdown) self.state_message = "%s%s" % (msg, message_shutdown)
for m in self.mcus: for cb in self.state_cb:
m.do_shutdown() cb('shutdown')
self.gcode.do_shutdown()
toolhead = self.objects.get('toolhead')
if toolhead is not None:
toolhead.do_shutdown()
def invoke_async_shutdown(self, msg): def invoke_async_shutdown(self, msg):
self.async_shutdown_msg = msg self.async_shutdown_msg = msg
self.request_exit("shutdown") self.request_exit("shutdown")

View File

@ -581,7 +581,7 @@ class MCU:
self._ffi_lib.steppersync_set_time(self._steppersync, 0., self._mcu_freq) self._ffi_lib.steppersync_set_time(self._steppersync, 0., self._mcu_freq)
for c in self._init_cmds: for c in self._init_cmds:
self.send(self.create_command(c)) self.send(self.create_command(c))
def connect(self): def _connect(self):
if self.is_fileoutput(): if self.is_fileoutput():
self._connect_file() self._connect_file()
else: else:
@ -666,9 +666,18 @@ class MCU:
def monotonic(self): def monotonic(self):
return self._reactor.monotonic() return self._reactor.monotonic()
# Restarts # 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): def _restart_arduino(self):
logging.info("Attempting MCU '%s' reset", self._name) logging.info("Attempting MCU '%s' reset", self._name)
self.disconnect() self._disconnect()
serialhdl.arduino_reset(self._serialport, self._reactor) serialhdl.arduino_reset(self._serialport, self._reactor)
def _restart_via_command(self): def _restart_via_command(self):
if ((self._reset_cmd is None and self._config_reset_cmd is None) 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 # Attempt reset via config_reset command
logging.info("Attempting MCU '%s' config_reset command", self._name) logging.info("Attempting MCU '%s' config_reset command", self._name)
self._is_shutdown = True self._is_shutdown = True
self.do_shutdown(force=True) self._shutdown(force=True)
self._reactor.pause(self._reactor.monotonic() + 0.015) self._reactor.pause(self._reactor.monotonic() + 0.015)
self.send(self._config_reset_cmd.encode()) self.send(self._config_reset_cmd.encode())
else: else:
@ -687,10 +696,10 @@ class MCU:
logging.info("Attempting MCU '%s' reset command", self._name) logging.info("Attempting MCU '%s' reset command", self._name)
self.send(self._reset_cmd.encode()) self.send(self._reset_cmd.encode())
self._reactor.pause(self._reactor.monotonic() + 0.015) self._reactor.pause(self._reactor.monotonic() + 0.015)
self.disconnect() self._disconnect()
def _restart_rpi_usb(self): def _restart_rpi_usb(self):
logging.info("Attempting MCU '%s' reset via rpi usb power", self._name) logging.info("Attempting MCU '%s' reset via rpi usb power", self._name)
self.disconnect() self._disconnect()
chelper.run_hub_ctrl(0) chelper.run_hub_ctrl(0)
self._reactor.pause(self._reactor.monotonic() + 2.) self._reactor.pause(self._reactor.monotonic() + 2.)
chelper.run_hub_ctrl(1) chelper.run_hub_ctrl(1)
@ -735,17 +744,15 @@ class MCU:
self._mcu_tick_stddev) self._mcu_tick_stddev)
return ' '.join([msg, self._serial.stats(eventtime), return ' '.join([msg, self._serial.stats(eventtime),
self._clocksync.stats(eventtime)]) self._clocksync.stats(eventtime)])
def do_shutdown(self, force=False): def printer_state(self, state):
if self._emergency_stop_cmd is None or (self._is_shutdown and not force): if state == 'connect':
return self._connect()
self.send(self._emergency_stop_cmd.encode()) elif state == 'disconnect':
def disconnect(self): self._disconnect()
self._serial.disconnect() elif state == 'shutdown':
if self._steppersync is not None: self._shutdown()
self._ffi_lib.steppersync_free(self._steppersync)
self._steppersync = None
def __del__(self): def __del__(self):
self.disconnect() self._disconnect()
Common_MCU_errors = { Common_MCU_errors = {
("Timer too close", "No next step", "Missed scheduling of next "): """ ("Timer too close", "No next step", "Missed scheduling of next "): """

View File

@ -369,12 +369,13 @@ class ToolHead:
buffer_time = max(0., self.print_time - est_print_time) buffer_time = max(0., self.print_time - est_print_time)
return "print_time=%.3f buffer_time=%.3f print_stall=%d" % ( return "print_time=%.3f buffer_time=%.3f print_stall=%d" % (
self.print_time, buffer_time, self.print_stall) self.print_time, buffer_time, self.print_stall)
def do_shutdown(self): def printer_state(self, state):
if state == 'shutdown':
try: try:
self.move_queue.reset() self.move_queue.reset()
self.reset_print_time() self.reset_print_time()
except: except:
logging.exception("Exception in do_shutdown") logging.exception("Exception in toolhead shutdown")
def get_kinematics(self): def get_kinematics(self):
return self.kin return self.kin
def get_max_velocity(self): def get_max_velocity(self):