Persist print recovery data on print failures

This commit is contained in:
Gina Häußge 2016-02-08 12:47:13 +01:00
parent f79d496c7c
commit 9e8b5312d5
4 changed files with 97 additions and 8 deletions

View file

@ -12,6 +12,7 @@ import octoprint.plugin
import octoprint.util
from octoprint.events import eventManager, Events
from octoprint.settings import settings
from .destinations import FileDestinations
from .analysis import QueueEntry, AnalysisQueue
@ -175,6 +176,8 @@ class FileManager(object):
self._progress_plugins = []
self._preprocessor_hooks = dict()
self._recovery_file = os.path.join(settings().getBaseFolder("data"), "print_recovery_data.yaml")
def initialize(self):
self.reload_plugins()
@ -414,6 +417,43 @@ class FileManager(object):
# if there's no storage configured where to log the print, we'll just not log it
pass
def save_recovery_data(self, origin, path, pos):
import time
import yaml
from octoprint.util import atomic_write
data = dict(origin=origin,
path=path,
pos=pos,
date=time.time())
try:
with atomic_write(self._recovery_file) as f:
yaml.safe_dump(data, stream=f, default_flow_style=False, indent=" ", allow_unicode=True)
except:
self._logger.exception("Could not write recovery data to file {}".format(self._recovery_file))
def delete_recovery_data(self):
if not os.path.isfile(self._recovery_file):
return
try:
os.remove(self._recovery_file)
except:
self._logger.exception("Error deleting recovery data file {}".format(self._recovery_file))
def get_recovery_data(self):
if not os.path.isfile(self._recovery_file):
return None
import yaml
try:
with open(self._recovery_file) as f:
data = yaml.safe_load(f)
return data
except:
self._logger.exception("Could not read recovery data from file {}".format(self._recovery_file))
self.delete_recovery_data()
def set_additional_metadata(self, destination, path, key, data, overwrite=False, merge=False):
self._storage(destination).set_additional_metadata(path, key, data, overwrite=overwrite, merge=merge)

View file

@ -214,7 +214,7 @@ class PrinterInterface(object):
"""
raise NotImplementedError()
def select_file(self, path, sd, printAfterSelect=False):
def select_file(self, path, sd, printAfterSelect=False, pos=None):
"""
Selects the specified ``path`` for printing, specifying if the file is to be found on the ``sd`` or not.
Optionally can also directly start the print after selecting the file.

View file

@ -67,6 +67,7 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback):
self._printTimeLeft = None
self._printAfterSelect = False
self._posAfterSelect = None
# sd handling
self._sdPrinting = False
@ -345,12 +346,23 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback):
factor = self._convert_rate_value(factor, min=75, max=125)
self.commands("M221 S%d" % factor)
def select_file(self, path, sd, printAfterSelect=False):
def select_file(self, path, sd, printAfterSelect=False, pos=None):
if self._comm is None or (self._comm.isBusy() or self._comm.isStreaming()):
self._logger.info("Cannot load file: printer not connected or currently busy")
return
recovery_data = self._fileManager.get_recovery_data()
if recovery_data:
# clean up recovery data if we just selected a different file than is logged in that
expected_origin = FileDestinations.SDCARD if sd else FileDestinations.LOCAL
actual_origin = recovery_data.get("origin", None)
actual_path = recovery_data.get("path", None)
if actual_origin is None or actual_path is None or actual_origin != expected_origin or actual_path != path:
self._fileManager.delete_recovery_data()
self._printAfterSelect = printAfterSelect
self._posAfterSelect = pos
self._comm.selectFile("/" + path if sd else path, sd)
self._setProgressData(0, None, None, None)
self._setCurrentZ(None)
@ -363,7 +375,7 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback):
self._setProgressData(0, None, None, None)
self._setCurrentZ(None)
def start_print(self):
def start_print(self, pos=None):
"""
Starts the currently loaded print job.
Only starts if the printer is connected and operational, not currently printing and a printjob is loaded
@ -388,10 +400,12 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback):
countdown = rolling_window
self._timeEstimationData = TimeEstimationHelper(rolling_window=rolling_window, threshold=threshold, countdown=countdown)
self._fileManager.delete_recovery_data()
self._lastProgressReport = None
self._setProgressData(0, None, None, None)
self._setCurrentZ(None)
self._comm.startPrint()
self._comm.startPrint(pos=pos)
def toggle_pause_print(self):
"""
@ -819,12 +833,13 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback):
self._stateMonitor.set_state({"text": self.get_state_string(), "flags": self._getStateFlags()})
if self._printAfterSelect:
self.start_print()
self.start_print(pos=self._posAfterSelect)
def on_comm_print_job_done(self):
self._fileManager.log_print(FileDestinations.SDCARD if self._selectedFile["sd"] else FileDestinations.LOCAL, self._selectedFile["filename"], time.time(), self._comm.getPrintTime(), True, self._printerProfileManager.get_current_or_default()["id"])
self._setProgressData(1.0, self._selectedFile["filesize"], self._comm.getPrintTime(), 0)
self._stateMonitor.set_state({"text": self.get_state_string(), "flags": self._getStateFlags()})
self._fileManager.delete_recovery_data()
def on_comm_file_transfer_started(self, filename, filesize):
self._sdStreaming = True
@ -849,6 +864,8 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback):
def on_comm_force_disconnect(self):
self.disconnect()
def on_comm_record_fileposition(self, origin, name, pos):
self._fileManager.save_recovery_data(origin, name, pos)
class StateMonitor(object):
def __init__(self, interval=0.5, on_update=None, on_add_temperature=None, on_add_log=None, on_add_message=None):

View file

@ -311,6 +311,7 @@ class MachineCom(object):
self._callback.on_comm_sd_files([])
if self._currentFile is not None:
self._recordFilePosition()
self._currentFile.close()
oldState = self.getStateString()
@ -558,7 +559,7 @@ class MachineCom(object):
self.sendCommand(line)
return "\n".join(scriptLines)
def startPrint(self):
def startPrint(self, pos=None):
if not self.isOperational() or self.isPrinting():
return
@ -586,19 +587,26 @@ class MachineCom(object):
self.sendGcodeScript("beforePrintStarted", replacements=dict(event=payload))
if self.isSdFileSelected():
#self.sendCommand("M26 S0") # setting the sd post apparently sometimes doesn't work, so we re-select
#self.sendCommand("M26 S0") # setting the sd pos apparently sometimes doesn't work, so we re-select
# the file instead
# make sure to ignore the "file selected" later on, otherwise we'll reset our progress data
self._ignore_select = True
self.sendCommand("M23 {filename}".format(filename=self._currentFile.getFilename()))
self._currentFile.setFilepos(0)
if pos is not None and isinstance(pos, int) and pos > 0:
self._currentFile.setFilepos(pos)
self.sendCommand("M26 S{}".format(pos))
else:
self._currentFile.setFilepos(0)
self.sendCommand("M24")
self._sd_status_timer = RepeatedTimer(lambda: get_interval("sdStatus", default_value=1.0), self._poll_sd_status, run_first=True)
self._sd_status_timer.start()
else:
if pos is not None and isinstance(pos, int) and pos > 0:
self._currentFile.seek(pos)
line = self._getNext()
if line is not None:
self.sendCommand(line)
@ -665,6 +673,8 @@ class MachineCom(object):
except:
pass
self._recordFilePosition()
payload = {
"file": self._currentFile.getFilename(),
"filename": os.path.basename(self._currentFile.getFilename()),
@ -792,6 +802,18 @@ class MachineCom(object):
self._callback.on_comm_sd_state_change(self._sdAvailable)
self._callback.on_comm_sd_files(self._sdFiles)
##~~ record aborted file positions
def _recordFilePosition(self):
if self._currentFile is None:
return
origin = self._currentFile.getFileLocation()
filename = self._currentFile.getFilename()
pos = self._currentFile.getFilepos()
self._callback.on_comm_record_fileposition(origin, filename, pos)
##~~ communication monitoring and handling
def _processTemperatures(self, line):
@ -1929,6 +1951,9 @@ class MachineComPrintCallback(object):
def on_comm_force_disconnect(self):
pass
def on_comm_record_fileposition(self, origin, name, pos):
pass
### Printing file information classes ##################################################################################
class PrintingFileInformation(object):
@ -2027,6 +2052,13 @@ class PrintingGcodeFileInformation(PrintingFileInformation):
self._size = os.stat(self._filename).st_size
self._pos = 0
def seek(self, offset):
if self._handle is None:
return
self._handle.seek(offset)
self._pos = self._handle.tell()
def start(self):
"""
Opens the file for reading and determines the file size.