Print time estimation is now not displayed until it becomes somewhat stable
This commit is contained in:
parent
85a567bdab
commit
f2562500a3
11 changed files with 564 additions and 196 deletions
|
|
@ -51,6 +51,8 @@
|
|||
long as they are in use
|
||||
* Settings in UI get refreshed when opening settings dialog
|
||||
* New event "SettingsUpdated"
|
||||
* Print time estimation is now not displayed until it becomes somewhat stable. Display in web interface now also happens
|
||||
in a fuzzy way instead of the format hh:mm:ss, to not suggest a high accuracy anymore where the can't be one
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
|
|
|
|||
|
|
@ -3,3 +3,4 @@ nose>=1.3.0
|
|||
sphinxcontrib-httpdomain
|
||||
sphinx_rtd_theme
|
||||
po2json
|
||||
ddt
|
||||
|
|
|
|||
|
|
@ -150,6 +150,8 @@ class GcodeAnalysisQueue(AbstractAnalysisQueue):
|
|||
result = dict()
|
||||
if self._gcode.totalMoveTimeMinute:
|
||||
result["estimatedPrintTime"] = self._gcode.totalMoveTimeMinute * 60
|
||||
if self._gcode.timeDistribution:
|
||||
result["printTimeDistribution"] = self._gcode.timeDistribution
|
||||
if self._gcode.extrusionAmount:
|
||||
result["filament"] = dict()
|
||||
for i in range(len(self._gcode.extrusionAmount)):
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ __author__ = "Gina Häußge <osd@foosel.net>"
|
|||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
|
||||
import time
|
||||
import datetime
|
||||
import threading
|
||||
import copy
|
||||
import os
|
||||
|
|
@ -34,6 +33,7 @@ class Printer():
|
|||
from collections import deque
|
||||
|
||||
self._logger = logging.getLogger(__name__)
|
||||
self._estimationLogger = logging.getLogger("ESTIMATIONS")
|
||||
|
||||
self._analysisQueue = analysisQueue
|
||||
self._fileManager = fileManager
|
||||
|
|
@ -73,6 +73,7 @@ class Printer():
|
|||
self._streamingFinishedCallback = None
|
||||
|
||||
self._selectedFile = None
|
||||
self._timeEstimationData = None
|
||||
|
||||
# comm
|
||||
self._comm = None
|
||||
|
|
@ -283,6 +284,7 @@ class Printer():
|
|||
if self._selectedFile is None:
|
||||
return
|
||||
|
||||
self._timeEstimationData = TimeEstimationHelper()
|
||||
self._setCurrentZ(None)
|
||||
self._comm.startPrint()
|
||||
|
||||
|
|
@ -347,16 +349,32 @@ class Printer():
|
|||
self._messages.append(message)
|
||||
self._stateMonitor.addMessage(message)
|
||||
|
||||
def _estimateTotalPrintTime(self, progress, printTime):
|
||||
if not progress or not printTime:
|
||||
return None
|
||||
|
||||
else:
|
||||
newEstimate = printTime / progress
|
||||
|
||||
if self._timeEstimationData.is_stable():
|
||||
return newEstimate
|
||||
|
||||
self._timeEstimationData.update(newEstimate)
|
||||
|
||||
return None
|
||||
|
||||
def _setProgressData(self, progress, filepos, printTime, printTimeLeft):
|
||||
estimatedTotalPrintTime = self._estimateTotalPrintTime(progress, printTime)
|
||||
|
||||
self._progress = progress
|
||||
self._printTime = printTime
|
||||
self._printTimeLeft = printTimeLeft
|
||||
self._printTimeLeft = estimatedTotalPrintTime - printTime if (estimatedTotalPrintTime is not None and printTime is not None) else None
|
||||
|
||||
self._stateMonitor.setProgress({
|
||||
"completion": self._progress * 100 if self._progress is not None else None,
|
||||
"filepos": filepos,
|
||||
"printTime": int(self._printTime) if self._printTime is not None else None,
|
||||
"printTimeLeft": int(self._printTimeLeft * 60) if self._printTimeLeft is not None else None
|
||||
"printTimeLeft": int(self._printTimeLeft) if self._printTimeLeft is not None else None
|
||||
})
|
||||
|
||||
def _addTemperatureData(self, temp, bedTemp):
|
||||
|
|
@ -388,7 +406,7 @@ class Printer():
|
|||
self._selectedFile = {
|
||||
"filename": filename,
|
||||
"filesize": filesize,
|
||||
"sd": sd
|
||||
"sd": sd,
|
||||
}
|
||||
else:
|
||||
self._selectedFile = None
|
||||
|
|
@ -778,3 +796,49 @@ class StateMonitor(object):
|
|||
"offsets": self._offsets
|
||||
}
|
||||
|
||||
|
||||
class TimeEstimationHelper(object):
|
||||
|
||||
STABLE_THRESHOLD = 0.1
|
||||
STABLE_COUNTDOWN = 100
|
||||
|
||||
def __init__(self):
|
||||
self._sum_total = 0
|
||||
self._sum_distance = 0
|
||||
self._count = 0
|
||||
self._stable_counter = None
|
||||
|
||||
def is_stable(self):
|
||||
return self._stable_counter is not None and self._stable_counter >= self.__class__.STABLE_COUNTDOWN
|
||||
|
||||
def update(self, newEstimate):
|
||||
old_average_total = self.average_total
|
||||
|
||||
self._sum_total += newEstimate
|
||||
self._count += 1
|
||||
|
||||
if old_average_total:
|
||||
self._sum_distance += 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:
|
||||
self._stable_counter = 0
|
||||
else:
|
||||
self._stable_counter += 1
|
||||
else:
|
||||
self._stable_counter = None
|
||||
|
||||
@property
|
||||
def average_total(self):
|
||||
if not self._count:
|
||||
return None
|
||||
else:
|
||||
return self._sum_total / self._count
|
||||
|
||||
@property
|
||||
def average_distance(self):
|
||||
if not self._count or self._count < 2:
|
||||
return None
|
||||
else:
|
||||
return self._sum_distance / (self._count - 1)
|
||||
|
||||
|
|
|
|||
|
|
@ -158,10 +158,21 @@ default_settings = {
|
|||
"okWithLinenumber": False,
|
||||
"numExtruders": 1,
|
||||
"includeCurrentToolInTemps": True,
|
||||
"movementSpeed": {
|
||||
"x": 6000,
|
||||
"y": 6000,
|
||||
"z": 200,
|
||||
"e": 300
|
||||
},
|
||||
"hasBed": True,
|
||||
"repetierStyleTargetTemperature": False,
|
||||
"smoothieTemperatureReporting": False,
|
||||
"extendedSdFileList": False
|
||||
"extendedSdFileList": False,
|
||||
"throttle": 0.01,
|
||||
"waitOnLongMoves": False,
|
||||
"rxBuffer": 64,
|
||||
"txBuffer": 40,
|
||||
"commandBuffer": 4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -342,6 +342,21 @@ function formatDuration(seconds) {
|
|||
return _.sprintf(gettext(/* L10N: duration format */ "%(hour)02d:%(minute)02d:%(second)02d"), {hour: h, minute: m, second: s});
|
||||
}
|
||||
|
||||
function formatFuzzyEstimation(seconds, base) {
|
||||
if (!seconds) return "-";
|
||||
if (seconds < 0) return "-";
|
||||
|
||||
var m;
|
||||
if (base != undefined) {
|
||||
m = moment(base);
|
||||
} else {
|
||||
m = moment();
|
||||
}
|
||||
|
||||
m.add(seconds, "s");
|
||||
return m.fromNow(true);
|
||||
}
|
||||
|
||||
function formatDate(unixTimestamp) {
|
||||
if (!unixTimestamp) return "-";
|
||||
return moment.unix(unixTimestamp).format(gettext(/* L10N: Date format */ "YYYY-MM-DD HH:mm"));
|
||||
|
|
|
|||
|
|
@ -62,9 +62,15 @@ function PrinterStateViewModel(loginStateViewModel) {
|
|||
return formatDuration(self.printTime());
|
||||
});
|
||||
self.printTimeLeftString = ko.computed(function() {
|
||||
if (!self.printTimeLeft())
|
||||
return "-";
|
||||
return formatDuration(self.printTimeLeft());
|
||||
if (self.printTimeLeft() == undefined) {
|
||||
if (!self.printTime() || !(self.isPrinting() || self.isPaused())) {
|
||||
return "-";
|
||||
} else {
|
||||
return gettext("Calculating...");
|
||||
}
|
||||
} else {
|
||||
return formatFuzzyEstimation(self.printTimeLeft());
|
||||
}
|
||||
});
|
||||
self.progressString = ko.computed(function() {
|
||||
if (!self.progress())
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ class gcode(object):
|
|||
|
||||
def _load(self, gcodeFile, printer_profile):
|
||||
filePos = 0
|
||||
readBytes = 0
|
||||
pos = [0.0, 0.0, 0.0]
|
||||
posOffset = [0.0, 0.0, 0.0]
|
||||
currentE = [0.0]
|
||||
|
|
@ -53,20 +54,25 @@ class gcode(object):
|
|||
absoluteE = True
|
||||
scale = 1.0
|
||||
posAbs = True
|
||||
feedRateXY = min(printer_profile["axes"]["x"], printer_profile["axes"]["y"])
|
||||
feedRateXY = min(printer_profile["axes"]["x"]["speed"], printer_profile["axes"]["y"]["speed"])
|
||||
offsets = printer_profile["extruder"]["offsets"]
|
||||
|
||||
for line in gcodeFile:
|
||||
if self._abort:
|
||||
raise AnalysisAborted()
|
||||
filePos += 1
|
||||
readBytes += len(line)
|
||||
|
||||
if isinstance(gcodeFile, (file)):
|
||||
percentage = float(readBytes) / float(self._fileSize)
|
||||
elif isinstance(gcodeFile, (list)):
|
||||
percentage = float(filePos) / float(len(gcodeFile))
|
||||
else:
|
||||
percentage = None
|
||||
|
||||
try:
|
||||
if self.progressCallback is not None and (filePos % 1000 == 0):
|
||||
if isinstance(gcodeFile, (file)):
|
||||
self.progressCallback(float(gcodeFile.tell()) / float(self._fileSize))
|
||||
elif isinstance(gcodeFile, (list)):
|
||||
self.progressCallback(float(filePos) / float(len(gcodeFile)))
|
||||
if self.progressCallback is not None and (filePos % 1000 == 0) and percentage is not None:
|
||||
self.progressCallback(percentage)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -9,21 +9,42 @@ import os
|
|||
import re
|
||||
import threading
|
||||
import math
|
||||
import Queue
|
||||
|
||||
from serial import SerialTimeoutException
|
||||
|
||||
from octoprint.settings import settings
|
||||
|
||||
class VirtualPrinter():
|
||||
def __init__(self):
|
||||
self.readList = ['start\n', 'Marlin: Virtual Marlin!\n', '\x80\n', 'SD init fail\n'] # no sd card as default startup scenario
|
||||
def __init__(self, read_timeout=5.0, write_timeout=10.0):
|
||||
self._read_timeout = read_timeout
|
||||
self._write_timeout = write_timeout
|
||||
|
||||
self.incoming = CharCountingQueue(settings().getInt(["devel", "virtualPrinter", "rxBuffer"]), name="RxBuffer")
|
||||
self.outgoing = Queue.Queue()
|
||||
self.buffered = Queue.Queue(maxsize=settings().getInt(["devel", "virtualPrinter", "commandBuffer"]))
|
||||
|
||||
for item in ['start\n', 'Marlin: Virtual Marlin!\n', '\x80\n', 'SD card ok\n']: # no sd card as default startup scenario
|
||||
self.outgoing.put(item)
|
||||
|
||||
self.currentExtruder = 0
|
||||
self.temp = [0.0] * settings().getInt(["devel", "virtualPrinter", "numExtruders"])
|
||||
self.targetTemp = [0.0] * settings().getInt(["devel", "virtualPrinter", "numExtruders"])
|
||||
self.lastTempAt = time.time()
|
||||
self.bedTemp = 1.0
|
||||
self.bedTargetTemp = 1.0
|
||||
self.speeds = settings().get(["devel", "virtualPrinter", "movementSpeed"])
|
||||
|
||||
self._relative = True
|
||||
self._lastX = None
|
||||
self._lastY = None
|
||||
self._lastZ = None
|
||||
self._lastE = None
|
||||
|
||||
self._unitModifier = 1
|
||||
|
||||
self._virtualSd = settings().getBaseFolder("virtualSd")
|
||||
self._sdCardReady = False
|
||||
self._sdCardReady = True
|
||||
self._sdPrinter = None
|
||||
self._sdPrintingSemaphore = threading.Event()
|
||||
self._selectedSdFile = None
|
||||
|
|
@ -36,151 +57,211 @@ class VirtualPrinter():
|
|||
self.currentLine = 0
|
||||
self.lastN = 0
|
||||
|
||||
self._incoming_lock = threading.RLock()
|
||||
|
||||
waitThread = threading.Thread(target=self._sendWaitAfterTimeout)
|
||||
waitThread.start()
|
||||
|
||||
def write(self, data):
|
||||
if self.readList is None:
|
||||
return
|
||||
readThread = threading.Thread(target=self._processIncoming)
|
||||
readThread.start()
|
||||
|
||||
data = data.strip()
|
||||
bufferThread = threading.Thread(target=self._processBuffer)
|
||||
bufferThread.start()
|
||||
|
||||
# strip checksum
|
||||
if "*" in data:
|
||||
data = data[:data.rfind("*")]
|
||||
self.currentLine += 1
|
||||
elif settings().getBoolean(["devel", "virtualPrinter", "forceChecksum"]):
|
||||
self.readList.append("Error: Missing checksum")
|
||||
return
|
||||
def __str__(self):
|
||||
return "VIRTUAL(read_timeout={read_timeout},write_timeout={write_timeout},options={options})"\
|
||||
.format(read_timeout=self._read_timeout, write_timeout=self._write_timeout, options=settings().get(["devel", "virtualPrinter"]))
|
||||
|
||||
# track N = N + 1
|
||||
if data.startswith("N") and "M110" in data:
|
||||
linenumber = int(re.search("N([0-9]+)", data).group(1))
|
||||
self.lastN = linenumber
|
||||
self.currentLine = linenumber
|
||||
self._sendOk()
|
||||
return
|
||||
elif data.startswith("N"):
|
||||
linenumber = int(re.search("N([0-9]+)", data).group(1))
|
||||
expected = self.lastN + 1
|
||||
if linenumber != expected:
|
||||
self.readList.append("Error: expected line %d got %d" % (expected, linenumber))
|
||||
self.readList.append("Resend:%d" % expected)
|
||||
if settings().getBoolean(["devel", "virtualPrinter", "okAfterResend"]):
|
||||
self.readList.append("ok")
|
||||
return
|
||||
elif self.currentLine == 100:
|
||||
# simulate a resend at line 100 of the last 5 lines
|
||||
self.lastN = 94
|
||||
self.readList.append("Error: Line Number is not Last Line Number\n")
|
||||
self.readList.append("rs %d\n" % (self.currentLine - 5))
|
||||
if settings().getBoolean(["devel", "virtualPrinter", "okAfterResend"]):
|
||||
self.readList.append("ok")
|
||||
return
|
||||
else:
|
||||
def _clearQueue(self, queue):
|
||||
try:
|
||||
while queue.get(block=False):
|
||||
continue
|
||||
except Queue.Empty:
|
||||
pass
|
||||
|
||||
def _processIncoming(self):
|
||||
while self.incoming is not None:
|
||||
self._simulateTemps()
|
||||
|
||||
try:
|
||||
data = self.incoming.get(timeout=0.01)
|
||||
except Queue.Empty:
|
||||
continue
|
||||
|
||||
if data is None:
|
||||
continue
|
||||
|
||||
data = data.strip()
|
||||
|
||||
# strip checksum
|
||||
if "*" in data:
|
||||
data = data[:data.rfind("*")]
|
||||
self.currentLine += 1
|
||||
elif settings().getBoolean(["devel", "virtualPrinter", "forceChecksum"]):
|
||||
self.outgoing.put("Error: Missing checksum")
|
||||
continue
|
||||
|
||||
# track N = N + 1
|
||||
if data.startswith("N") and "M110" in data:
|
||||
linenumber = int(re.search("N([0-9]+)", data).group(1))
|
||||
self.lastN = linenumber
|
||||
data = data.split(None, 1)[1].strip()
|
||||
self.currentLine = linenumber
|
||||
self._sendOk()
|
||||
continue
|
||||
elif data.startswith("N"):
|
||||
linenumber = int(re.search("N([0-9]+)", data).group(1))
|
||||
expected = self.lastN + 1
|
||||
if linenumber != expected:
|
||||
with self._incoming_lock:
|
||||
self._clearQueue(self.incoming)
|
||||
self.outgoing.put("Error: expected line %d got %d" % (expected, linenumber))
|
||||
self.outgoing.put("Resend:%d" % expected)
|
||||
self.outgoing.put("ok")
|
||||
continue
|
||||
elif self.currentLine == 100:
|
||||
# simulate a resend at line 100
|
||||
with self._incoming_lock:
|
||||
self.lastN = 99
|
||||
self._clearQueue(self.incoming)
|
||||
self.outgoing.put("Error: Line Number is not Last Line Number\n")
|
||||
self.outgoing.put("rs 100\n")
|
||||
self.outgoing.put("ok")
|
||||
continue
|
||||
else:
|
||||
self.lastN = linenumber
|
||||
data = data.split(None, 1)[1].strip()
|
||||
|
||||
data += "\n"
|
||||
data += "\n"
|
||||
|
||||
# shortcut for writing to SD
|
||||
if self._writingToSd and not self._selectedSdFile is None and not "M29" in data:
|
||||
with open(self._selectedSdFile, "a") as f:
|
||||
f.write(data)
|
||||
self._sendOk()
|
||||
return
|
||||
# shortcut for writing to SD
|
||||
if self._writingToSd and not self._selectedSdFile is None and not "M29" in data:
|
||||
with open(self._selectedSdFile, "a") as f:
|
||||
f.write(data)
|
||||
self._sendOk()
|
||||
continue
|
||||
|
||||
#print "Send: %s" % (data.rstrip())
|
||||
if 'M104' in data or 'M109' in data:
|
||||
self._parseHotendCommand(data)
|
||||
return
|
||||
#print "Send: %s" % (data.rstrip())
|
||||
if 'M104' in data or 'M109' in data:
|
||||
self._parseHotendCommand(data)
|
||||
continue
|
||||
|
||||
if 'M140' in data or 'M190' in data:
|
||||
self._parseBedCommand(data)
|
||||
return
|
||||
if 'M140' in data or 'M190' in data:
|
||||
self._parseBedCommand(data)
|
||||
continue
|
||||
|
||||
if 'M105' in data:
|
||||
self._processTemperatureQuery()
|
||||
elif 'M20' in data:
|
||||
if self._sdCardReady:
|
||||
self._listSd()
|
||||
elif 'M21' in data:
|
||||
self._sdCardReady = True
|
||||
self.readList.append("SD card ok")
|
||||
elif 'M22' in data:
|
||||
self._sdCardReady = False
|
||||
elif 'M23' in data:
|
||||
if self._sdCardReady:
|
||||
filename = data.split(None, 1)[1].strip()
|
||||
self._selectSdFile(filename)
|
||||
elif 'M24' in data:
|
||||
if self._sdCardReady:
|
||||
self._startSdPrint()
|
||||
elif 'M25' in data:
|
||||
if self._sdCardReady:
|
||||
self._pauseSdPrint()
|
||||
elif 'M26' in data:
|
||||
if self._sdCardReady:
|
||||
pos = int(re.search("S([0-9]+)", data).group(1))
|
||||
self._setSdPos(pos)
|
||||
elif 'M27' in data:
|
||||
if self._sdCardReady:
|
||||
self._reportSdStatus()
|
||||
elif 'M28' in data:
|
||||
if self._sdCardReady:
|
||||
filename = data.split(None, 1)[1].strip()
|
||||
self._writeSdFile(filename)
|
||||
elif 'M29' in data:
|
||||
if self._sdCardReady:
|
||||
self._finishSdFile()
|
||||
elif 'M30' in data:
|
||||
if self._sdCardReady:
|
||||
filename = data.split(None, 1)[1].strip()
|
||||
self._deleteSdFile(filename)
|
||||
elif "M114" in data:
|
||||
# send dummy position report
|
||||
self.readList.append("ok C: X:10.00 Y:3.20 Z:5.20 E:1.24")
|
||||
elif "M117" in data:
|
||||
# we'll just use this to echo a message, to allow playing around with pause triggers
|
||||
self.readList.append("ok %s" % re.search("M117\s+(.*)", data).group(1))
|
||||
elif "M999" in data:
|
||||
# mirror Marlin behaviour
|
||||
self.readList.append("Resend: 1")
|
||||
elif data.startswith("T"):
|
||||
self.currentExtruder = int(re.search("T(\d+)", data).group(1))
|
||||
self._sendOk()
|
||||
self.readList.append("Active Extruder: %d" % self.currentExtruder)
|
||||
elif len(data.strip()) > 0:
|
||||
self._sendOk()
|
||||
if 'M105' in data:
|
||||
self._processTemperatureQuery()
|
||||
continue
|
||||
elif 'M20' in data:
|
||||
if self._sdCardReady:
|
||||
self._listSd()
|
||||
elif 'M21' in data:
|
||||
self._sdCardReady = True
|
||||
self.outgoing.put("SD card ok")
|
||||
elif 'M22' in data:
|
||||
self._sdCardReady = False
|
||||
elif 'M23' in data:
|
||||
if self._sdCardReady:
|
||||
filename = data.split(None, 1)[1].strip()
|
||||
self._selectSdFile(filename)
|
||||
elif 'M24' in data:
|
||||
if self._sdCardReady:
|
||||
self._startSdPrint()
|
||||
elif 'M25' in data:
|
||||
if self._sdCardReady:
|
||||
self._pauseSdPrint()
|
||||
elif 'M26' in data:
|
||||
if self._sdCardReady:
|
||||
pos = int(re.search("S([0-9]+)", data).group(1))
|
||||
self._setSdPos(pos)
|
||||
elif 'M27' in data:
|
||||
if self._sdCardReady:
|
||||
self._reportSdStatus()
|
||||
elif 'M28' in data:
|
||||
if self._sdCardReady:
|
||||
filename = data.split(None, 1)[1].strip()
|
||||
self._writeSdFile(filename)
|
||||
elif 'M29' in data:
|
||||
if self._sdCardReady:
|
||||
self._finishSdFile()
|
||||
elif 'M30' in data:
|
||||
if self._sdCardReady:
|
||||
filename = data.split(None, 1)[1].strip()
|
||||
self._deleteSdFile(filename)
|
||||
elif "M114" in data:
|
||||
# send dummy position report
|
||||
self.outgoing.put("ok C: X:10.00 Y:3.20 Z:5.20 E:1.24")
|
||||
continue
|
||||
elif "M117" in data:
|
||||
# we'll just use this to echo a message, to allow playing around with pause triggers
|
||||
self.outgoing.put("echo:%s" % re.search("M117\s+(.*)", data).group(1))
|
||||
elif "M999" in data:
|
||||
# mirror Marlin behaviour
|
||||
self.outgoing.put("Resend: 1")
|
||||
elif data.startswith("T"):
|
||||
self.currentExtruder = int(re.search("T(\d+)", data).group(1))
|
||||
self.outgoing.put("Active Extruder: %d" % self.currentExtruder)
|
||||
elif "G20" in data:
|
||||
self._unitModifier = 1.0 / 2.54
|
||||
if self._lastX is not None:
|
||||
self._lastX *= 2.54
|
||||
if self._lastY is not None:
|
||||
self._lastY *= 2.54
|
||||
if self._lastZ is not None:
|
||||
self._lastZ *= 2.54
|
||||
if self._lastE is not None:
|
||||
self._lastE *= 2.54
|
||||
elif "G21" in data:
|
||||
self._unitModifier = 1.0
|
||||
if self._lastX is not None:
|
||||
self._lastX /= 2.54
|
||||
if self._lastY is not None:
|
||||
self._lastY /= 2.54
|
||||
if self._lastZ is not None:
|
||||
self._lastZ /= 2.54
|
||||
if self._lastE is not None:
|
||||
self._lastE /= 2.54
|
||||
elif "G90" in data:
|
||||
self._relative = False
|
||||
elif "G91" in data:
|
||||
self._relative = True
|
||||
elif "G92" in data:
|
||||
self._setPosition(data)
|
||||
|
||||
elif data.startswith("G0") or data.startswith("G1") or data.startswith("G2") or data.startswith("G3") \
|
||||
or data.startswith("G28") or data.startswith("G29") or data.startswith("G30") \
|
||||
or data.startswith("G31") or data.startswith("G32"):
|
||||
# simulate reprap buffered commands via a Queue with maxsize which internally simulates the moves
|
||||
self.buffered.put(data)
|
||||
|
||||
if len(data.strip()) > 0:
|
||||
self._sendOk()
|
||||
|
||||
def _listSd(self):
|
||||
self.readList.append("Begin file list")
|
||||
self.outgoing.put("Begin file list")
|
||||
if settings().getBoolean(["devel", "virtualPrinter", "extendedSdFileList"]):
|
||||
self.readList.extend(
|
||||
map(
|
||||
lambda x: "%s %d" % (x.upper(), os.stat(os.path.join(self._virtualSd, x)).st_size),
|
||||
os.listdir(self._virtualSd)
|
||||
)
|
||||
items = map(
|
||||
lambda x: "%s %d" % (x.upper(), os.stat(os.path.join(self._virtualSd, x)).st_size),
|
||||
os.listdir(self._virtualSd)
|
||||
)
|
||||
else:
|
||||
self.readList.extend(
|
||||
map(
|
||||
lambda x: x.upper(),
|
||||
os.listdir(self._virtualSd)
|
||||
)
|
||||
items = map(
|
||||
lambda x: x.upper(),
|
||||
os.listdir(self._virtualSd)
|
||||
)
|
||||
self.readList.append("End file list")
|
||||
self._sendOk()
|
||||
for item in items:
|
||||
self.outgoing.put(item)
|
||||
self.outgoing.put("End file list")
|
||||
|
||||
def _selectSdFile(self, filename):
|
||||
file = os.path.join(self._virtualSd, filename).lower()
|
||||
if not os.path.exists(file) or not os.path.isfile(file):
|
||||
self.readList.append("open failed, File: %s." % filename)
|
||||
self.outgoing.put("open failed, File: %s." % filename)
|
||||
else:
|
||||
self._selectedSdFile = file
|
||||
self._selectedSdFileSize = os.stat(file).st_size
|
||||
self.readList.append("File opened: %s Size: %d" % (filename, self._selectedSdFileSize))
|
||||
self.readList.append("File selected")
|
||||
self.outgoing.put("File opened: %s Size: %d" % (filename, self._selectedSdFileSize))
|
||||
self.outgoing.put("File selected")
|
||||
|
||||
def _startSdPrint(self):
|
||||
if self._selectedSdFile is not None:
|
||||
|
|
@ -188,20 +269,18 @@ class VirtualPrinter():
|
|||
self._sdPrinter = threading.Thread(target=self._sdPrintingWorker)
|
||||
self._sdPrinter.start()
|
||||
self._sdPrintingSemaphore.set()
|
||||
self._sendOk()
|
||||
|
||||
def _pauseSdPrint(self):
|
||||
self._sdPrintingSemaphore.clear()
|
||||
self._sendOk()
|
||||
|
||||
def _setSdPos(self, pos):
|
||||
self._newSdFilePos = pos
|
||||
|
||||
def _reportSdStatus(self):
|
||||
if self._sdPrinter is not None and self._sdPrintingSemaphore.is_set:
|
||||
self.readList.append("SD printing byte %d/%d" % (self._selectedSdFilePos, self._selectedSdFileSize))
|
||||
self.outgoing.put("SD printing byte %d/%d" % (self._selectedSdFilePos, self._selectedSdFileSize))
|
||||
else:
|
||||
self.readList.append("Not SD printing")
|
||||
self.outgoing.put("Not SD printing")
|
||||
|
||||
def _processTemperatureQuery(self):
|
||||
includeTarget = not settings().getBoolean(["devel", "virtualPrinter", "repetierStyleTargetTemperature"])
|
||||
|
|
@ -224,16 +303,16 @@ class VirtualPrinter():
|
|||
|
||||
if settings().getBoolean(["devel", "virtualPrinter", "includeCurrentToolInTemps"]):
|
||||
if includeTarget:
|
||||
self.readList.append("ok T:%.2f /%.2f %s @:64\n" % (self.temp[self.currentExtruder], self.targetTemp[self.currentExtruder] + 1, allTempsString))
|
||||
self.outgoing.put("ok T:%.2f /%.2f %s @:64\n" % (self.temp[self.currentExtruder], self.targetTemp[self.currentExtruder] + 1, allTempsString))
|
||||
else:
|
||||
self.readList.append("ok T:%.2f %s @:64\n" % (self.temp[self.currentExtruder], allTempsString))
|
||||
self.outgoing.put("ok T:%.2f %s @:64\n" % (self.temp[self.currentExtruder], allTempsString))
|
||||
else:
|
||||
self.readList.append("ok %s @:64\n" % allTempsString)
|
||||
self.outgoing.put("ok %s @:64\n" % allTempsString)
|
||||
else:
|
||||
if includeTarget:
|
||||
self.readList.append("ok T:%.2f /%.2f B:%.2f /%.2f @:64\n" % (self.temp[0], self.targetTemp[0], self.bedTemp, self.bedTargetTemp))
|
||||
self.outgoing.put("ok T:%.2f /%.2f B:%.2f /%.2f @:64\n" % (self.temp[0], self.targetTemp[0], self.bedTemp, self.bedTargetTemp))
|
||||
else:
|
||||
self.readList.append("ok T:%.2f B:%.2f @:64\n" % (self.temp[0], self.bedTemp))
|
||||
self.outgoing.put("ok T:%.2f B:%.2f @:64\n" % (self.temp[0], self.bedTemp))
|
||||
|
||||
def _parseHotendCommand(self, line):
|
||||
tool = 0
|
||||
|
|
@ -254,14 +333,10 @@ class VirtualPrinter():
|
|||
pass
|
||||
|
||||
if "M109" in line:
|
||||
self._heatupThread = threading.Thread(target=self._waitForHeatup, args=["tool%d" % tool])
|
||||
self._heatupThread.start()
|
||||
return
|
||||
|
||||
self._sendOk()
|
||||
|
||||
self._waitForHeatup("tool%d" % tool)
|
||||
if settings().getBoolean(["devel", "virtualPrinter", "repetierStyleTargetTemperature"]):
|
||||
self.readList.append("TargetExtr%d:%d" % (tool, self.targetTemp[tool]))
|
||||
self.outgoing.put("TargetExtr%d:%d" % (tool, self.targetTemp[tool]))
|
||||
self._sendOk()
|
||||
|
||||
def _parseBedCommand(self, line):
|
||||
try:
|
||||
|
|
@ -270,14 +345,98 @@ class VirtualPrinter():
|
|||
pass
|
||||
|
||||
if "M190" in line:
|
||||
self._heatupThread = threading.Thread(target=self._waitForHeatup, args=["bed"])
|
||||
self._heatupThread.start()
|
||||
return
|
||||
|
||||
self._waitForHeatup("bed")
|
||||
if settings().getBoolean(["devel", "virtualPrinter", "repetierStyleTargetTemperature"]):
|
||||
self.outgoing.put("TargetBed:%d" % self.bedTargetTemp)
|
||||
self._sendOk()
|
||||
|
||||
if settings().getBoolean(["devel", "virtualPrinter", "repetierStyleTargetTemperature"]):
|
||||
self.readList.append("TargetBed:%d" % self.bedTargetTemp)
|
||||
def _performMove(self, line):
|
||||
matchX = re.search("X([0-9.]+)", line)
|
||||
matchY = re.search("Y([0-9.]+)", line)
|
||||
matchZ = re.search("Z([0-9.]+)", line)
|
||||
matchE = re.search("E([0-9.]+)", line)
|
||||
|
||||
duration = 0
|
||||
if matchX is not None:
|
||||
try:
|
||||
x = float(matchX.group(1))
|
||||
if self._relative or self._lastX is None:
|
||||
duration = max(duration, x * self._unitModifier / float(self.speeds["x"]) * 60.0)
|
||||
else:
|
||||
duration = max(duration, (x - self._lastX) * self._unitModifier / float(self.speeds["x"]) * 60.0)
|
||||
self._lastX = x
|
||||
except:
|
||||
pass
|
||||
if matchY is not None:
|
||||
try:
|
||||
y = float(matchY.group(1))
|
||||
if self._relative or self._lastY is None:
|
||||
duration = max(duration, y * self._unitModifier / float(self.speeds["y"]) * 60.0)
|
||||
else:
|
||||
duration = max(duration, (y - self._lastY) * self._unitModifier / float(self.speeds["y"]) * 60.0)
|
||||
self._lastY = y
|
||||
except:
|
||||
pass
|
||||
if matchZ is not None:
|
||||
try:
|
||||
z = float(matchZ.group(1))
|
||||
if self._relative or self._lastZ is None:
|
||||
duration = max(duration, z * self._unitModifier / float(self.speeds["z"]) * 60.0)
|
||||
else:
|
||||
duration = max(duration, (z - self._lastZ) * self._unitModifier / float(self.speeds["z"]) * 60.0)
|
||||
self._lastZ = z
|
||||
except:
|
||||
pass
|
||||
if matchE is not None:
|
||||
try:
|
||||
e = float(matchE.group(1))
|
||||
if self._relative or self._lastE is None:
|
||||
duration = max(duration, e * self._unitModifier / float(self.speeds["e"]) * 60.0)
|
||||
else:
|
||||
duration = max(duration, (e - self._lastE) * self._unitModifier / float(self.speeds["e"]) * 60.0)
|
||||
self._lastE = e
|
||||
except:
|
||||
pass
|
||||
|
||||
if duration:
|
||||
if settings().getBoolean(["devel", "virtualPrinter", "waitOnLongMoves"]):
|
||||
slept = 0
|
||||
while duration - slept > self._read_timeout:
|
||||
time.sleep(self._read_timeout)
|
||||
self.outgoing.put("wait")
|
||||
slept += self._read_timeout
|
||||
else:
|
||||
time.sleep(duration)
|
||||
|
||||
def _setPosition(self, line):
|
||||
matchX = re.search("X([0-9.]+)", line)
|
||||
matchY = re.search("Y([0-9.]+)", line)
|
||||
matchZ = re.search("Z([0-9.]+)", line)
|
||||
matchE = re.search("E([0-9.]+)", line)
|
||||
|
||||
if matchX is None and matchY is None and matchZ is None and matchE is None:
|
||||
self._lastX = self._lastY = self._lastZ = self._lastE = 0
|
||||
else:
|
||||
if matchX is not None:
|
||||
try:
|
||||
self._lastX = float(matchX.group(1))
|
||||
except:
|
||||
pass
|
||||
if matchY is not None:
|
||||
try:
|
||||
self._lastY = float(matchY.group(1))
|
||||
except:
|
||||
pass
|
||||
if matchZ is not None:
|
||||
try:
|
||||
self._lastZ = float(matchZ.group(1))
|
||||
except:
|
||||
pass
|
||||
if matchE is not None:
|
||||
try:
|
||||
self._lastE = float(matchE.group(1))
|
||||
except:
|
||||
pass
|
||||
|
||||
def _writeSdFile(self, filename):
|
||||
file = os.path.join(self._virtualSd, filename).lower()
|
||||
|
|
@ -285,17 +444,15 @@ class VirtualPrinter():
|
|||
if os.path.isfile(file):
|
||||
os.remove(file)
|
||||
else:
|
||||
self.readList.append("error writing to file")
|
||||
self.outgoing.put("error writing to file")
|
||||
|
||||
self._writingToSd = True
|
||||
self._selectedSdFile = file
|
||||
self.readList.append("Writing to file: %s" % filename)
|
||||
self._sendOk()
|
||||
self.outgoing.put("Writing to file: %s" % filename)
|
||||
|
||||
def _finishSdFile(self):
|
||||
self._writingToSd = False
|
||||
self._selectedSdFile = None
|
||||
self._sendOk()
|
||||
|
||||
def _sdPrintingWorker(self):
|
||||
self._selectedSdFilePos = 0
|
||||
|
|
@ -318,47 +475,45 @@ class VirtualPrinter():
|
|||
if 'M140' in line or 'M190' in line:
|
||||
self._parseBedCommand(line)
|
||||
|
||||
time.sleep(0.01)
|
||||
time.sleep(settings().getFloat(["devel", "virtualPrinter", "throttle"]))
|
||||
|
||||
self._sdPrintingSemaphore.clear()
|
||||
self._selectedSdFilePos = 0
|
||||
self._sdPrinter = None
|
||||
self.readList.append("Done printing file")
|
||||
self.outgoing.put("Done printing file")
|
||||
|
||||
def _waitForHeatup(self, heater):
|
||||
delta = 0.5
|
||||
delta = 1
|
||||
delay = 1
|
||||
if heater.startswith("tool"):
|
||||
toolNum = int(heater[len("tool"):])
|
||||
while self.temp[toolNum] < self.targetTemp[toolNum] - delta or self.temp[toolNum] > self.targetTemp[toolNum] + delta:
|
||||
self._simulateTemps()
|
||||
self.readList.append("T:%0.2f /%0.2f" % (self.temp[toolNum], self.targetTemp[toolNum]))
|
||||
self._simulateTemps(delta=delta)
|
||||
self.outgoing.put("T:%0.2f" % self.temp[toolNum])
|
||||
time.sleep(delay)
|
||||
elif heater == "bed":
|
||||
while self.bedTemp < self.bedTargetTemp - delta or self.bedTemp > self.bedTargetTemp + delta:
|
||||
self._simulateTemps()
|
||||
self.readList.append("B:%0.2f /%0.2f" % (self.bedTemp, self.bedTargetTemp))
|
||||
self._simulateTemps(delta=delta)
|
||||
self.outgoing.put("B:%0.2f" % self.bedTemp)
|
||||
time.sleep(delay)
|
||||
self.readList.append("ok")
|
||||
|
||||
def _deleteSdFile(self, filename):
|
||||
f = os.path.join(self._virtualSd, filename)
|
||||
if os.path.exists(f) and os.path.isfile(f):
|
||||
os.remove(f)
|
||||
self._sendOk()
|
||||
|
||||
def _simulateTemps(self):
|
||||
def _simulateTemps(self, delta=1):
|
||||
timeDiff = self.lastTempAt - time.time()
|
||||
self.lastTempAt = time.time()
|
||||
for i in range(len(self.temp)):
|
||||
if abs(self.temp[i] - self.targetTemp[i]) > 1:
|
||||
if abs(self.temp[i] - self.targetTemp[i]) > delta:
|
||||
oldVal = self.temp[i]
|
||||
self.temp[i] += math.copysign(timeDiff * 10, self.targetTemp[i] - self.temp[i])
|
||||
if math.copysign(1, self.targetTemp[i] - oldVal) != math.copysign(1, self.targetTemp[i] - self.temp[i]):
|
||||
self.temp[i] = self.targetTemp[i]
|
||||
if self.temp[i] < 0:
|
||||
self.temp[i] = 0
|
||||
if abs(self.bedTemp - self.bedTargetTemp) > 1:
|
||||
if abs(self.bedTemp - self.bedTargetTemp) > delta:
|
||||
oldVal = self.bedTemp
|
||||
self.bedTemp += math.copysign(timeDiff * 10, self.bedTargetTemp - self.bedTemp)
|
||||
if math.copysign(1, self.bedTargetTemp - oldVal) != math.copysign(1, self.bedTargetTemp - self.bedTemp):
|
||||
|
|
@ -366,34 +521,98 @@ class VirtualPrinter():
|
|||
if self.bedTemp < 0:
|
||||
self.bedTemp = 0
|
||||
|
||||
def _processBuffer(self):
|
||||
while self.buffered is not None:
|
||||
try:
|
||||
line = self.buffered.get(timeout=0.5)
|
||||
except Queue.Empty:
|
||||
continue
|
||||
|
||||
if line is None:
|
||||
continue
|
||||
|
||||
self._performMove(line)
|
||||
|
||||
def write(self, data):
|
||||
with self._incoming_lock:
|
||||
if self.incoming is None or self.outgoing is None:
|
||||
return
|
||||
try:
|
||||
self.incoming.put(data, timeout=self._write_timeout)
|
||||
except Queue.Full:
|
||||
raise SerialTimeoutException()
|
||||
|
||||
def readline(self):
|
||||
if self.readList is None:
|
||||
return ''
|
||||
n = 0
|
||||
|
||||
self._simulateTemps()
|
||||
|
||||
while len(self.readList) < 1:
|
||||
time.sleep(0.1)
|
||||
n += 1
|
||||
if n == 20:
|
||||
return ''
|
||||
if self.readList is None:
|
||||
return ''
|
||||
time.sleep(0.001)
|
||||
return self.readList.pop(0)
|
||||
try:
|
||||
line = self.outgoing.get(timeout=self._read_timeout)
|
||||
time.sleep(settings().getFloat(["devel", "virtualPrinter", "throttle"]))
|
||||
return line
|
||||
except Queue.Empty:
|
||||
return ""
|
||||
|
||||
def close(self):
|
||||
self.readList = None
|
||||
self.incoming = None
|
||||
self.outgoing = None
|
||||
self.buffered = None
|
||||
|
||||
def _sendOk(self):
|
||||
if settings().getBoolean(["devel", "virtualPrinter", "okWithLinenumber"]):
|
||||
self.readList.append("ok %d" % self.lastN)
|
||||
self.outgoing.put("ok %d" % self.lastN)
|
||||
else:
|
||||
self.readList.append("ok")
|
||||
self.outgoing.put("ok")
|
||||
|
||||
def _sendWaitAfterTimeout(self, timeout=5):
|
||||
time.sleep(timeout)
|
||||
if self.readList is not None:
|
||||
self.readList.append("wait")
|
||||
if self.outgoing is not None:
|
||||
self.outgoing.put("wait")
|
||||
|
||||
class CharCountingQueue(Queue.Queue):
|
||||
|
||||
def __init__(self, maxsize, name=None):
|
||||
Queue.Queue.__init__(self, maxsize=maxsize)
|
||||
self._size = 0
|
||||
self._name = name
|
||||
|
||||
def put(self, item, block=True, timeout=None):
|
||||
self.not_full.acquire()
|
||||
try:
|
||||
item_size = self._len(item)
|
||||
|
||||
if not block:
|
||||
if self._qsize() + item_size >= self.maxsize:
|
||||
raise Queue.Full
|
||||
elif timeout is None:
|
||||
while self._qsize() + item_size >= self.maxsize:
|
||||
self.not_full.wait()
|
||||
elif timeout < 0:
|
||||
raise ValueError("'timeout' must be a positive number")
|
||||
else:
|
||||
endtime = time.time() + timeout
|
||||
while self._qsize() + item_size >= self.maxsize:
|
||||
remaining = endtime - time.time()
|
||||
if remaining <= 0.0:
|
||||
raise Queue.Full
|
||||
self.not_full.wait(remaining)
|
||||
|
||||
self._put(item)
|
||||
self.unfinished_tasks += 1
|
||||
self.not_empty.notify()
|
||||
finally:
|
||||
self.not_full.release()
|
||||
|
||||
def _len(self, item):
|
||||
return len(item)
|
||||
|
||||
def _qsize(self, len=len):
|
||||
return self._size
|
||||
|
||||
# Put a new item in the queue
|
||||
def _put(self, item):
|
||||
self.queue.append(item)
|
||||
self._size += self._len(item)
|
||||
|
||||
# Get an item from the queue
|
||||
def _get(self):
|
||||
item = self.queue.popleft()
|
||||
self._size -= self._len(item)
|
||||
return item
|
||||
|
|
|
|||
42
tests/printer/test_estimation.py
Normal file
42
tests/printer/test_estimation.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License"
|
||||
|
||||
|
||||
import unittest
|
||||
from ddt import ddt, data, unpack
|
||||
|
||||
import octoprint.printer
|
||||
|
||||
@ddt
|
||||
class EstimationTestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.estimation_helper = octoprint.printer.TimeEstimationHelper()
|
||||
|
||||
@data(
|
||||
((1.0, 2.0, 3.0, 4.0, 5.0), 3.0),
|
||||
((1.0, 2.0, 0.0, 1.0, 2.0), 1.2),
|
||||
((1.0, -2.0, -1.0, -2.0, 3.0), -0.2)
|
||||
)
|
||||
@unpack
|
||||
def test_average_total(self, estimates, expected):
|
||||
for estimate in estimates:
|
||||
self.estimation_helper.update(estimate)
|
||||
|
||||
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
|
||||
)
|
||||
@unpack
|
||||
def test_average_distance(self, estimates, expected):
|
||||
for estimate in estimates:
|
||||
self.estimation_helper.update(estimate)
|
||||
|
||||
self.assertEquals(self.estimation_helper.average_distance, expected)
|
||||
|
||||
Loading…
Reference in a new issue