klippy: Rework shutdown handling

If an MCU signals a shutdown from the background thread, notify the
main thread and handle the shutdown there.  Dispatch shutdown handling
from the main Printer() class instead of from the Toolhead class.
This simplifies the shutdown logic.

Signed-off-by: Kevin O'Connor <kevin@koconnor.net>
This commit is contained in:
Kevin O'Connor 2017-10-12 15:15:14 -04:00
parent 3033b03789
commit f8750b142f
4 changed files with 72 additions and 63 deletions

View File

@ -23,8 +23,9 @@ class GCodeParser:
self.bytes_read = 0 self.bytes_read = 0
self.input_log = collections.deque([], 50) self.input_log = collections.deque([], 50)
# Command handling # Command handling
self.gcode_handlers = self.build_handlers(False)
self.is_printer_ready = False self.is_printer_ready = False
self.gcode_handlers = {}
self.build_handlers()
self.need_ack = False self.need_ack = False
self.toolhead = self.fan = self.extruder = None self.toolhead = self.fan = self.extruder = None
self.heaters = [] self.heaters = []
@ -34,28 +35,21 @@ class GCodeParser:
self.last_position = [0.0, 0.0, 0.0, 0.0] self.last_position = [0.0, 0.0, 0.0, 0.0]
self.homing_add = [0.0, 0.0, 0.0, 0.0] self.homing_add = [0.0, 0.0, 0.0, 0.0]
self.axis2pos = {'X': 0, 'Y': 1, 'Z': 2, 'E': 3} self.axis2pos = {'X': 0, 'Y': 1, 'Z': 2, 'E': 3}
def build_handlers(self, is_ready): def build_handlers(self):
handlers = self.all_handlers handlers = self.all_handlers
if not is_ready: if not self.is_printer_ready:
handlers = [h for h in handlers handlers = [h for h in handlers
if getattr(self, 'cmd_'+h+'_when_not_ready', False)] if getattr(self, 'cmd_'+h+'_when_not_ready', False)]
gcode_handlers = { h: getattr(self, 'cmd_'+h) for h in handlers } gcode_handlers = { h: getattr(self, 'cmd_'+h) for h in handlers }
for h, f in list(gcode_handlers.items()): for h, f in list(gcode_handlers.items()):
aliases = getattr(self, 'cmd_'+h+'_aliases', []) aliases = getattr(self, 'cmd_'+h+'_aliases', [])
gcode_handlers.update({ a: f for a in aliases }) gcode_handlers.update({ a: f for a in aliases })
return gcode_handlers self.gcode_handlers = gcode_handlers
def stats(self, eventtime): def stats(self, eventtime):
return "gcodein=%d" % (self.bytes_read,) return "gcodein=%d" % (self.bytes_read,)
def set_printer_ready(self, is_ready): def connect(self):
if self.is_printer_ready == is_ready: self.is_printer_ready = True
return self.build_handlers()
self.is_printer_ready = is_ready
self.gcode_handlers = self.build_handlers(is_ready)
if not is_ready:
# Printer is shutdown (could be running in a background thread)
if self.is_fileinput:
self.printer.request_exit()
return
# Lookup printer components # Lookup printer components
self.toolhead = self.printer.objects.get('toolhead') self.toolhead = self.printer.objects.get('toolhead')
extruders = extruder.get_printer_extruders(self.printer) extruders = extruder.get_printer_extruders(self.printer)
@ -67,6 +61,14 @@ class GCodeParser:
self.fan = self.printer.objects.get('fan') self.fan = self.printer.objects.get('fan')
if self.is_fileinput and self.fd_handle is None: if self.is_fileinput and self.fd_handle is None:
self.fd_handle = self.reactor.register_fd(self.fd, self.process_data) self.fd_handle = self.reactor.register_fd(self.fd, self.process_data)
def do_shutdown(self):
if not self.is_printer_ready:
return
self.is_printer_ready = False
self.build_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
@ -114,12 +116,10 @@ class GCodeParser:
except error as e: except error as e:
self.respond_error(str(e)) self.respond_error(str(e))
except: except:
logging.exception("Exception in command handler") msg = 'Internal error on command:"%s"' % (cmd,)
self.toolhead.force_shutdown() logging.exception(msg)
self.respond_error('Internal error on command:"%s"' % (cmd,)) self.printer.invoke_shutdown(msg)
if self.is_fileinput: self.respond_error(msg)
self.printer.request_exit()
break
self.ack() self.ack()
self.need_ack = prev_need_ack self.need_ack = prev_need_ack
def process_data(self, eventtime): def process_data(self, eventtime):
@ -396,7 +396,7 @@ class GCodeParser:
self.set_temp(params, wait=True) self.set_temp(params, wait=True)
def cmd_M112(self, params): def cmd_M112(self, params):
# Emergency Stop # Emergency Stop
self.toolhead.force_shutdown() self.printer.invoke_shutdown("Shutdown due to M112 command")
cmd_M114_when_not_ready = True cmd_M114_when_not_ready = True
def cmd_M114(self, params): def cmd_M114(self, params):
# Get Current Position # Get Current Position

View File

@ -136,18 +136,15 @@ class Printer:
self.connect_timer = self.reactor.register_timer( self.connect_timer = self.reactor.register_timer(
self._connect, self.reactor.NOW) self._connect, self.reactor.NOW)
self.all_config_options = {} self.all_config_options = {}
self.need_dump_debug = False
self.state_message = message_startup self.state_message = message_startup
self.is_shutdown = False
self.async_shutdown_msg = ""
self.run_result = None self.run_result = None
self.fileconfig = None self.fileconfig = None
self.mcus = [] self.mcus = []
def get_start_args(self): def get_start_args(self):
return self.start_args return self.start_args
def _stats(self, eventtime, force_output=False): def _stats(self, eventtime, force_output=False):
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
toolhead = self.objects.get('toolhead') toolhead = self.objects.get('toolhead')
if toolhead is None: if toolhead is None:
return eventtime + 1. return eventtime + 1.
@ -196,7 +193,7 @@ class Printer:
self._load_config() self._load_config()
for m in self.mcus: for m in self.mcus:
m.connect() m.connect()
self.gcode.set_printer_ready(True) self.gcode.connect()
self.state_message = message_ready self.state_message = message_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)
@ -219,34 +216,46 @@ class Printer:
monotime = self.reactor.monotonic() monotime = self.reactor.monotonic()
logging.info("Start printer at %s (%.1f %.1f)", logging.info("Start printer at %s (%.1f %.1f)",
time.asctime(time.localtime(systime)), systime, monotime) time.asctime(time.localtime(systime)), systime, monotime)
# Enter main reactor loop while 1:
try: # Enter main reactor loop
self.reactor.run() try:
except: self.reactor.run()
logging.exception("Unhandled exception during run") except:
return "exit" logging.exception("Unhandled exception during run")
# Check restart flags return "exit"
run_result = self.run_result # Check restart flags
try: run_result = self.run_result
self._stats(self.reactor.monotonic(), force_output=True) try:
for m in self.mcus: if run_result == 'shutdown':
if run_result == 'firmware_restart': self.invoke_shutdown(self.async_shutdown_msg, True)
m.microcontroller_restart() continue
m.disconnect() self._stats(self.reactor.monotonic(), force_output=True)
except: for m in self.mcus:
logging.exception("Unhandled exception during post run") if run_result == 'firmware_restart':
return run_result m.microcontroller_restart()
m.disconnect()
except:
logging.exception("Unhandled exception during post run")
return run_result
def get_state_message(self): def get_state_message(self):
return self.state_message return self.state_message
def note_shutdown(self, msg): def invoke_shutdown(self, msg, is_mcu_shutdown=False):
if self.state_message == message_ready: if self.is_shutdown:
self.need_dump_debug = True return
self.state_message = "%s%s" % (msg, message_shutdown) self.is_shutdown = True
self.gcode.set_printer_ready(False) if is_mcu_shutdown:
def note_mcu_error(self, msg): self.state_message = "%s%s" % (msg, message_shutdown)
self.state_message = "%s%s" % (msg, message_restart) else:
self.gcode.set_printer_ready(False) self.state_message = "%s%s" % (msg, message_restart)
self.gcode.motor_heater_off() 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()
def invoke_async_shutdown(self, msg):
self.async_shutdown_msg = msg
self.request_exit("shutdown")
def request_exit(self, result="exit"): def request_exit(self, result="exit"):
self.run_result = result self.run_result = result
self.reactor.end() self.reactor.end()

View File

@ -472,7 +472,7 @@ class MCU:
prefix = "MCU '%s' shutdown: " % (self._name,) prefix = "MCU '%s' shutdown: " % (self._name,)
if params['#name'] == 'is_shutdown': if params['#name'] == 'is_shutdown':
prefix = "Previous MCU '%s' shutdown: " % (self._name,) prefix = "Previous MCU '%s' shutdown: " % (self._name,)
self._printer.note_shutdown(prefix + msg + error_help(msg)) self._printer.invoke_async_shutdown(prefix + msg + error_help(msg))
# Connection phase # Connection phase
def _check_restart(self, reason): def _check_restart(self, reason):
start_reason = self._printer.get_start_args().get("start_reason") start_reason = self._printer.get_start_args().get("start_reason")
@ -723,11 +723,11 @@ class MCU:
return return
offset, freq = self._clocksync.calibrate_clock(print_time, eventtime) offset, freq = self._clocksync.calibrate_clock(print_time, eventtime)
self._ffi_lib.steppersync_set_time(self._steppersync, offset, freq) self._ffi_lib.steppersync_set_time(self._steppersync, offset, freq)
if self._clocksync.is_active(eventtime): if self._clocksync.is_active(eventtime) or self.is_fileoutput():
return return
logging.info("Timeout with MCU '%s' (eventtime=%f)", logging.info("Timeout with MCU '%s' (eventtime=%f)",
self._name, eventtime) self._name, eventtime)
self._printer.note_mcu_error("Lost communication with MCU '%s'" % ( self._printer.invoke_shutdown("Lost communication with MCU '%s'" % (
self._name,)) self._name,))
def stats(self, eventtime): def stats(self, eventtime):
msg = "%s: mcu_awake=%.03f mcu_task_avg=%.06f mcu_task_stddev=%.06f" % ( msg = "%s: mcu_awake=%.03f mcu_task_avg=%.06f mcu_task_stddev=%.06f" % (
@ -735,7 +735,9 @@ 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 force_shutdown(self): def do_shutdown(self):
if self._is_shutdown or self._emergency_stop_cmd is None:
return
self.send(self._emergency_stop_cmd.encode()) self.send(self._emergency_stop_cmd.encode())
def disconnect(self): def disconnect(self):
self._serial.disconnect() self._serial.disconnect()

View File

@ -292,7 +292,7 @@ class ToolHead:
self.last_flush_from_idle = True self.last_flush_from_idle = True
except: except:
logging.exception("Exception in flush_handler") logging.exception("Exception in flush_handler")
self.force_shutdown() self.printer.invoke_shutdown("Exception in flush_handler")
return self.reactor.NEVER return self.reactor.NEVER
# Motor off timer # Motor off timer
def _motor_off_handler(self, eventtime): def _motor_off_handler(self, eventtime):
@ -305,7 +305,7 @@ class ToolHead:
self.motor_off() self.motor_off()
except: except:
logging.exception("Exception in motor_off_handler") logging.exception("Exception in motor_off_handler")
self.force_shutdown() self.printer.invoke_shutdown("Exception in motor_off_handler")
return eventtime + self.motor_off_time return eventtime + self.motor_off_time
# Movement commands # Movement commands
def get_position(self): def get_position(self):
@ -371,14 +371,12 @@ 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 force_shutdown(self): def do_shutdown(self):
try: try:
for m in self.all_mcus:
m.force_shutdown()
self.move_queue.reset() self.move_queue.reset()
self.reset_print_time() self.reset_print_time()
except: except:
logging.exception("Exception in force_shutdown") logging.exception("Exception in do_shutdown")
def get_max_velocity(self): def get_max_velocity(self):
return self.max_velocity, self.max_accel return self.max_velocity, self.max_accel
def get_max_axis_halt(self): def get_max_axis_halt(self):