diff --git a/src/octoprint/printer/__init__.py b/src/octoprint/printer/__init__.py index 542c55bd..8b1c25cf 100644 --- a/src/octoprint/printer/__init__.py +++ b/src/octoprint/printer/__init__.py @@ -377,24 +377,31 @@ class Printer(): def _estimateTotalPrintTime(self, progress, printTime): if not progress or not printTime: + #self._estimationLogger.info("{progress};{printTime};;;".format(**locals())) return None else: newEstimate = printTime / progress if self._timeEstimationData.is_stable(): + #self._estimationLogger.info("{progress};{printTime};{newEstimate};;".format(**locals())) return newEstimate self._timeEstimationData.update(newEstimate) + #averageTotal = self._timeEstimationData.average_total + #averageDistance = self._timeEstimationData.average_distance + # + #self._estimationLogger.info("{progress};{printTime};{newEstimate};{averageTotal};{averageDistance}".format(**locals())) + return None - def _setProgressData(self, progress, filepos, printTime, printTimeLeft): - estimatedTotalPrintTime = self._estimateTotalPrintTime(progress, printTime) + def _setProgressData(self, progress, filepos, printTime, cleanedPrintTime): + estimatedTotalPrintTime = self._estimateTotalPrintTime(progress, cleanedPrintTime) self._progress = progress self._printTime = printTime - self._printTimeLeft = estimatedTotalPrintTime - printTime if (estimatedTotalPrintTime is not None and printTime is not None) else None + self._printTimeLeft = estimatedTotalPrintTime - cleanedPrintTime if (estimatedTotalPrintTime is not None and cleanedPrintTime is not None) else None self._stateMonitor.setProgress({ "completion": self._progress * 100 if self._progress is not None else None, @@ -558,7 +565,7 @@ class Printer(): Triggers storage of new values for printTime, printTimeLeft and the current progress. """ - self._setProgressData(self._comm.getPrintProgress(), self._comm.getPrintFilepos(), self._comm.getPrintTime(), self._comm.getPrintTimeRemainingEstimate()) + self._setProgressData(self._comm.getPrintProgress(), self._comm.getPrintFilepos(), self._comm.getPrintTime(), self._comm.getCleanedPrintTime()) def mcZChange(self, newZ): """ @@ -833,11 +840,13 @@ class StateMonitor(object): class TimeEstimationHelper(object): STABLE_THRESHOLD = 0.1 - STABLE_COUNTDOWN = 100 + STABLE_COUNTDOWN = 250 + STABLE_ROLLING_WINDOW = 250 def __init__(self): + import collections + self._distances = collections.deque([], self.__class__.STABLE_ROLLING_WINDOW) self._sum_total = 0 - self._sum_distance = 0 self._count = 0 self._stable_counter = None @@ -851,7 +860,7 @@ class TimeEstimationHelper(object): self._count += 1 if old_average_total: - self._sum_distance += abs(self.average_total - old_average_total) + self._distances.append(abs(self.average_total - old_average_total)) if -1.0 * self.__class__.STABLE_THRESHOLD < self.average_distance < self.__class__.STABLE_THRESHOLD: if self._stable_counter is None: @@ -870,8 +879,8 @@ class TimeEstimationHelper(object): @property def average_distance(self): - if not self._count or self._count < 2: + if not self._count or self._count < self.__class__.STABLE_ROLLING_WINDOW + 1: return None else: - return self._sum_distance / (self._count - 1) + return sum(self._distances) / len(self._distances) diff --git a/src/octoprint/util/comm.py b/src/octoprint/util/comm.py index eef72246..ee51f9b1 100644 --- a/src/octoprint/util/comm.py +++ b/src/octoprint/util/comm.py @@ -143,8 +143,10 @@ class MachineCom(object): self._bedTempOffset = 0 self._commandQueue = queue.Queue() self._currentZ = None - self._heatupWaitStartTime = 0 + self._heatupWaitStartTime = None self._heatupWaitTimeLost = 0.0 + self._pauseWaitStartTime = None + self._pauseWaitTimeLost = 0.0 self._currentExtruder = 0 self._alwaysSendChecksum = settings().getBoolean(["feature", "alwaysSendChecksum"]) @@ -314,20 +316,17 @@ class MachineCom(object): if self._currentFile is None or self._currentFile.getStartTime() is None: return None else: - return time.time() - self._currentFile.getStartTime() + return time.time() - self._currentFile.getStartTime() - self._pauseWaitTimeLost - def getPrintTimeRemainingEstimate(self): + def getCleanedPrintTime(self): printTime = self.getPrintTime() if printTime is None: return None - printTime /= 60 - progress = self._currentFile.getProgress() - if progress: - printTimeTotal = printTime / progress - return printTimeTotal - printTime - else: - return None + cleanedPrintTime = printTime - self._heatupWaitTimeLost + if cleanedPrintTime < 0: + cleanedPrintTime = 0.0 + return cleanedPrintTime def getTemp(self): return self._temp @@ -388,6 +387,11 @@ class MachineCom(object): if self._currentFile is None: raise ValueError("No file selected for printing") + self._heatupWaitStartTime = 0 + self._heatupWaitTimeLost = 0.0 + self._pauseWaitStartTime = 0 + self._pauseWaitTimeLost = 0.0 + try: self._currentFile.start() @@ -470,6 +474,10 @@ class MachineCom(object): return if not pause and self.isPaused(): + if self._pauseWaitStartTime: + self._pauseWaitTimeLost = self._pauseWaitTimeLost + (time.time() - self._pauseWaitStartTime) + self._pauseWaitStartTime = None + self._changeState(self.STATE_PRINTING) if self.isSdFileSelected(): self.sendCommand("M24") @@ -482,6 +490,9 @@ class MachineCom(object): "origin": self._currentFile.getFileLocation() }) elif pause and self.isPrinting(): + if not self._pauseWaitStartTime: + self._pauseWaitStartTime = time.time() + self._changeState(self.STATE_PAUSED) if self.isSdFileSelected(): self.sendCommand("M25") # pause print @@ -688,12 +699,9 @@ class MachineCom(object): self._callback.mcTempUpdate(self._temp, self._bedTemp) #If we are waiting for an M109 or M190 then measure the time we lost during heatup, so we can remove that time from our printing time estimate. - if not 'ok' in line: - heatingUp = True - if self._heatupWaitStartTime != 0: - t = time.time() - self._heatupWaitTimeLost = t - self._heatupWaitStartTime - self._heatupWaitStartTime = t + if 'ok' in line and self._heatupWaitStartTime: + self._heatupWaitTimeLost = self._heatupWaitTimeLost + (time.time() - self._heatupWaitStartTime) + self._heatupWaitStartTime = None elif supportRepetierTargetTemp and ('TargetExtr' in line or 'TargetBed' in line): matchExtr = self._regex_repetierTempExtr.match(line) matchBed = self._regex_repetierTempBed.match(line) @@ -1242,10 +1250,6 @@ class MachineCom(object): self.cancelPrint() return cmd - def _gcode_M112(self, cmd): # It's an emergency what todo? Canceling the print should be the minimum - self.cancelPrint() - return cmd - ### MachineCom callback ################################################################################################ @@ -1386,9 +1390,9 @@ class PrintingGcodeFileInformation(PrintingFileInformation): """ Opens the file for reading and determines the file size. Start time won't be recorded until 100 lines in """ + PrintingFileInformation.start(self) self._filehandle = open(self._filename, "r") self._lineCount = None - self._startTime = None def getNext(self): """ @@ -1415,9 +1419,6 @@ class PrintingGcodeFileInformation(PrintingFileInformation): self._lineCount += 1 self._filepos = self._filehandle.tell() - if self._lineCount >= 100 and self._startTime is None: - self._startTime = time.time() - return processedLine except Exception as (e): if self._filehandle is not None: diff --git a/tests/printer/test_estimation.py b/tests/printer/test_estimation.py index fb9fd5c9..5096d9e8 100644 --- a/tests/printer/test_estimation.py +++ b/tests/printer/test_estimation.py @@ -15,7 +15,10 @@ import octoprint.printer class EstimationTestCase(unittest.TestCase): def setUp(self): - self.estimation_helper = octoprint.printer.TimeEstimationHelper() + self.estimation_helper = type(octoprint.printer.TimeEstimationHelper)(octoprint.printer.TimeEstimationHelper.__name__, (octoprint.printer.TimeEstimationHelper,), { + 'STABLE_ROLLING_WINDOW': 3, + 'STABLE_COUNTDOWN': 1 + })() @data( ((1.0, 2.0, 3.0, 4.0, 5.0), 3.0), @@ -30,8 +33,8 @@ class EstimationTestCase(unittest.TestCase): self.assertEquals(self.estimation_helper.average_total, expected) @data( - ((1.0, 2.0, 3.0, 4.0, 5.0), 0.5), # average totals: 1.0, 1.5, 2.0, 2.5, 3.0 - ((1.0, 2.0, 0.0, 1.0, 2.0), 0.3) # average totals: 1.0, 1.5, 1.0, 1.0, 1.2 + ((1.0, 2.0, 3.0, 4.0, 5.0), 0.5), # average totals: 1.0, 1.5, 2.0, 2.5, 3.0 => (0.5 + 0.5 + 0.5) / 3 = 0.5 + ((1.0, 2.0, 0.0, 1.0, 2.0), 0.7 / 3) # average totals: 1.0, 1.5, 1.0, 1.0, 1.2 => (0.5 + 0.0 + 0.2) / 3 = 0.7 / 3 ) @unpack def test_average_distance(self, estimates, expected): @@ -40,3 +43,16 @@ class EstimationTestCase(unittest.TestCase): self.assertEquals(self.estimation_helper.average_distance, expected) + @data( + ((1.0, 1.0, 1.0, 1.0), False), # average totals: 1.0, 1.0, 1.0, 1.0 => 3.0 / 3 = 1.0 + ((1.0, 1.0, 1.0, 1.0, 1.0), True), # average totals: 1.0, 1.0, 1.0, 1.0, 1.0 => 0.0 / 3 = 0.0 + ((1.0, 2.0, 3.0, 4.0, 5.0), False), # average totals: 1.0, 1.5, 2.0, 2.5, 3.0 => 1.5 / 3 = 0.5 + ((0.0, 0.09, 0.18, 0.27, 0.36), True) # average totals: 0.0, 0.045, 0.09, 0.135, 0.18 => (0.045 + 0.045 + 0.045) / 3 = 0.045 + ) + @unpack + def test_is_stable(self, estimates, expected): + for estimate in estimates: + self.estimation_helper.update(estimate) + + self.assertEquals(self.estimation_helper.is_stable(), expected) +