From 9e8b5312d507bb2cd0115f156f130d163250eb8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 8 Feb 2016 12:47:13 +0100 Subject: [PATCH] Persist print recovery data on print failures --- src/octoprint/filemanager/__init__.py | 40 +++++++++++++++++++++++++++ src/octoprint/printer/__init__.py | 2 +- src/octoprint/printer/standard.py | 25 ++++++++++++++--- src/octoprint/util/comm.py | 38 +++++++++++++++++++++++-- 4 files changed, 97 insertions(+), 8 deletions(-) diff --git a/src/octoprint/filemanager/__init__.py b/src/octoprint/filemanager/__init__.py index 34f31680..23cef921 100644 --- a/src/octoprint/filemanager/__init__.py +++ b/src/octoprint/filemanager/__init__.py @@ -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) diff --git a/src/octoprint/printer/__init__.py b/src/octoprint/printer/__init__.py index 0a4195da..9265f598 100644 --- a/src/octoprint/printer/__init__.py +++ b/src/octoprint/printer/__init__.py @@ -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. diff --git a/src/octoprint/printer/standard.py b/src/octoprint/printer/standard.py index 8b909705..1d50e519 100644 --- a/src/octoprint/printer/standard.py +++ b/src/octoprint/printer/standard.py @@ -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): diff --git a/src/octoprint/util/comm.py b/src/octoprint/util/comm.py index 5b9a1aab..ba0a1721 100644 --- a/src/octoprint/util/comm.py +++ b/src/octoprint/util/comm.py @@ -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.