Persist print recovery data on print failures
This commit is contained in:
parent
f79d496c7c
commit
9e8b5312d5
4 changed files with 97 additions and 8 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue