Changed to GCODE streaming instead of loading it all into memory -- no more loading times. Also better file handling.

master
Gina Häußge 2013-05-31 22:41:53 +02:00
parent 8d53f313fe
commit 56cb1c294c
6 changed files with 397 additions and 401 deletions

View File

@ -57,18 +57,17 @@ class Printer():
self._printTime = None
self._printTimeLeft = None
# gcode handling
self._gcodeList = None
self._filename = None
self._gcodeLoader = None
self._printAfterSelect = False
# sd handling
self._sdPrinting = False
self._sdStreaming = False
# TODO Still needed?
self._sdFile = None
self._sdStreamer = None
# feedrate
self._feedrateModifierMapping = {"outerWall": "WALL-OUTER", "innerWall": "WALL_INNER", "fill": "FILL", "support": "SUPPORT"}
self._selectedFile = None
# timelapse
self._timelapse = None
@ -89,10 +88,8 @@ class Printer():
)
self._stateMonitor.reset(
state={"state": None, "stateString": self.getStateString(), "flags": self._getStateFlags()},
jobData={"filename": None, "lines": None, "estimatedPrintTime": None, "filament": None},
gcodeData={"filename": None, "progress": None},
sdUploadData={"filename": None, "progress": None},
progress={"progress": None, "printTime": None, "printTimeLeft": None},
jobData={"filename": None, "filesize": None, "estimatedPrintTime": None, "filament": None},
progress={"progress": None, "filepos": None, "printTime": None, "printTimeLeft": None},
currentZ=None
)
@ -163,52 +160,25 @@ class Printer():
for command in commands:
self._comm.sendCommand(command)
def setFeedrateModifier(self, structure, percentage):
if (not self._feedrateModifierMapping.has_key(structure)) or percentage < 0:
def selectFile(self, filename, sd, printAfterSelect=False):
if self._comm is not None and (self._comm.isBusy() or self._comm.isStreaming()):
return
self._comm.setFeedrateModifier(self._feedrateModifierMapping[structure], percentage / 100.0)
self._printAfterSelect = printAfterSelect
self._comm.selectFile(filename, sd)
def loadGcode(self, file, printAfterLoading=False):
"""
Loads the gcode from the given file as the new print job.
Aborts if the printer is currently printing or another gcode file is currently being loaded.
"""
if (self._comm is not None and self._comm.isPrinting()) or (self._gcodeLoader is not None):
return
self._sdFile = None
self._setJobData(None, None)
onGcodeLoadedCallback = self._onGcodeLoaded
if printAfterLoading:
onGcodeLoadedCallback = self._onGcodeLoadedToPrint
self._gcodeLoader = GcodeLoader(file, self._onGcodeLoadingProgress, onGcodeLoadedCallback)
self._gcodeLoader.start()
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
def startPrint(self):
"""
Starts the currently loaded print job.
Only starts if the printer is connected and operational, not currently printing and a printjob is loaded
"""
if self._comm is None or not self._comm.isOperational():
if self._comm is None or not self._comm.isOperational() or self._comm.isPrinting():
return
if self._gcodeList is None and self._sdFile is None:
return
if self._comm.isPrinting():
if self._selectedFile is None:
return
self._setCurrentZ(-1)
if self._sdFile is not None:
# we are working in sd mode
self._sdPrinting = True
self._comm.printSdFile()
else:
# we are working in local mode
self._comm.printGCode(self._gcodeList)
self._comm.startPrint()
def togglePausePrint(self):
"""
@ -216,6 +186,7 @@ class Printer():
"""
if self._comm is None:
return
self._comm.setPause(not self._comm.isPaused())
def cancelPrint(self, disableMotorsAndHeater=True):
@ -225,22 +196,18 @@ class Printer():
if self._comm is None:
return
if self._sdPrinting:
self._sdPrinting = False
self._comm.cancelPrint()
if disableMotorsAndHeater:
self.commands(["M84", "M104 S0", "M140 S0", "M106 S0"]) # disable motors, switch off heaters and fan
# reset line, height, print time
# reset progress, height, print time
self._setCurrentZ(None)
self._setProgressData(None, None, None, None)
# mark print as failure
if self._filename is not None:
self._gcodeManager.printFailed(self._filename)
#~~ state monitoring
if self._selectedFile is not None:
self._gcodeManager.printFailed(self._selectedFile["filename"])
def setTimelapse(self, timelapse):
if self._timelapse is not None and self.isPrinting():
@ -251,6 +218,8 @@ class Printer():
def getTimelapse(self):
return self._timelapse
#~~ state monitoring
def _setCurrentZ(self, currentZ):
self._currentZ = currentZ
@ -273,7 +242,7 @@ class Printer():
self._messages = self._messages[-300:]
self._stateMonitor.addMessage(message)
def _setProgressData(self, progress, currentLine, printTime, printTimeLeft):
def _setProgressData(self, progress, filepos, printTime, printTimeLeft):
self._progress = progress
self._printTime = printTime
self._printTimeLeft = printTimeLeft
@ -286,7 +255,11 @@ class Printer():
if (self._printTimeLeft):
formattedPrintTimeLeft = util.getFormattedTimeDelta(datetime.timedelta(minutes=self._printTimeLeft))
self._stateMonitor.setProgress({"progress": self._progress, "currentLine": currentLine, "printTime": formattedPrintTime, "printTimeLeft": formattedPrintTimeLeft})
formattedFilePos = None
if (filepos):
formattedFilePos = util.getFormattedSize(filepos)
self._stateMonitor.setProgress({"progress": self._progress, "filepos": formattedFilePos, "printTime": formattedPrintTime, "printTimeLeft": formattedPrintTimeLeft})
def _addTemperatureData(self, temp, bedTemp, targetTemp, bedTargetTemp):
currentTimeUtc = int(time.time() * 1000)
@ -310,19 +283,25 @@ class Printer():
self._stateMonitor.addTemperature({"currentTime": currentTimeUtc, "temp": self._temp, "bedTemp": self._bedTemp, "targetTemp": self._targetTemp, "targetBedTemp": self._targetBedTemp})
def _setJobData(self, filename, gcodeList):
self._filename = filename
self._gcodeList = gcodeList
lines = None
if self._gcodeList:
lines = len(self._gcodeList)
def _setJobData(self, filename, filesize, sd):
if filename is not None:
self._selectedFile = {
"filename": filename,
"filesize": filesize,
"sd": sd
}
else:
self._selectedFile = None
formattedFilename = None
formattedFilesize = None
estimatedPrintTime = None
filament = None
if self._filename:
formattedFilename = os.path.basename(self._filename)
if filename:
formattedFilename = os.path.basename(filename)
if filesize:
formattedFilesize = util.getFormattedSize(filesize)
fileData = self._gcodeManager.getFileData(filename)
if fileData is not None and "gcodeAnalysis" in fileData.keys():
@ -331,7 +310,7 @@ class Printer():
if "filament" in fileData["gcodeAnalysis"].keys():
filament = fileData["gcodeAnalysis"]["filament"]
self._stateMonitor.setJobData({"filename": formattedFilename, "lines": lines, "estimatedPrintTime": estimatedPrintTime, "filament": filament})
self._stateMonitor.setJobData({"filename": formattedFilename, "filesize": formattedFilesize, "estimatedPrintTime": estimatedPrintTime, "filament": filament, "sd": sd})
def _sendInitialStateUpdate(self, callback):
try:
@ -358,7 +337,6 @@ class Printer():
"printing": self.isPrinting(),
"closedOrError": self.isClosedOrError(),
"error": self.isError(),
"loading": self.isLoading(),
"paused": self.isPaused(),
"ready": self.isReady(),
"sdReady": sdReady
@ -386,14 +364,15 @@ class Printer():
if oldState == self._comm.STATE_PRINTING and state != self._comm.STATE_PAUSED:
self._timelapse.onPrintjobStopped()
elif state == self._comm.STATE_PRINTING and oldState != self._comm.STATE_PAUSED:
self._timelapse.onPrintjobStarted(self._filename)
self._timelapse.onPrintjobStarted(self._selectedFile["filename"])
# forward relevant state changes to gcode manager
if self._comm is not None and oldState == self._comm.STATE_PRINTING:
if state == self._comm.STATE_OPERATIONAL:
self._gcodeManager.printSucceeded(self._filename)
elif state == self._comm.STATE_CLOSED or state == self._comm.STATE_ERROR or state == self._comm.STATE_CLOSED_WITH_ERROR:
self._gcodeManager.printFailed(self._filename)
if self._selectedFile is not None:
if state == self._comm.STATE_OPERATIONAL:
self._gcodeManager.printSucceeded(self._selectedFile["filename"])
elif state == self._comm.STATE_CLOSED or state == self._comm.STATE_ERROR or state == self._comm.STATE_CLOSED_WITH_ERROR:
self._gcodeManager.printFailed(self._selectedFile["filename"])
self._gcodeManager.resumeAnalysis() # printing done, put those cpu cycles to good use
elif self._comm is not None and state == self._comm.STATE_PRINTING:
self._gcodeManager.pauseAnalysis() # do not analyse gcode while printing
@ -410,25 +389,10 @@ class Printer():
def mcProgress(self):
"""
Callback method for the comm object, called upon any change in progress of the printjob.
Triggers storage of new values for printTime, printTimeLeft and the current line.
Triggers storage of new values for printTime, printTimeLeft and the current progress.
"""
oldProgress = self._progress
if self._sdPrinting:
newLine = None
(filePos, fileSize) = self._comm.getSdProgress()
if fileSize > 0:
newProgress = float(filePos) / float(fileSize)
else:
newProgress = 0.0
else:
newLine = self._comm.getPrintPos()
if self._gcodeList is not None:
newProgress = float(newLine) / float(len(self._gcodeList))
else:
newProgress = 0.0
self._setProgressData(newProgress, newLine, self._comm.getPrintTime(), self._comm.getPrintTimeRemainingEstimate())
self._setProgressData(self._comm.getPrintProgress(), self._comm.getPrintFilepos(), self._comm.getPrintTime(), self._comm.getPrintTimeRemainingEstimate())
def mcZChange(self, newZ):
"""
@ -446,33 +410,49 @@ class Printer():
def mcSdFiles(self, files):
self._sendTriggerUpdateCallbacks("gcodeFiles")
def mcSdSelected(self, filename, filesize):
self._sdFile = filename
self._setJobData(filename, None)
def mcFileSelected(self, filename, filesize, sd):
self._setJobData(filename, filesize, sd)
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
if self._sdPrintAfterSelect:
if self._printAfterSelect:
self.startPrint()
def mcSdPrintingDone(self):
self._sdPrinting = False
self._setProgressData(1.0, None, self._comm.getPrintTime(), self._comm.getPrintTimeRemainingEstimate())
def mcPrintjobDone(self):
self._setProgressData(1.0, self._selectedFile["filesize"], self._comm.getPrintTime(), 0)
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
#~~ sd file handling
def mcFileTransferStarted(self, filename, filesize):
self._sdStreaming = True
self._selectedFile = {
"filename": filename,
"filesize": filesize,
"sd": True
}
self._setJobData(filename, filesize, True)
self._setProgressData(0.0, 0, 0, None)
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
def mcFileTransferDone(self):
self._sdStreaming = False
self._selectedFile = None
self._setCurrentZ(None)
self._setJobData(None, None, None)
self._setProgressData(None, None, None, None)
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
#~~ sd file handling
def getSdFiles(self):
if self._comm is None:
return
return self._comm.getSdFiles()
def addSdFile(self, filename, file):
if not self._comm:
def addSdFile(self, filename, path):
if not self._comm or self._comm.isBusy():
return
self._sdStreamer = SdFileStreamer(self._comm, filename, file, self._onSdFileStreamProgress, self._onSdFileStreamFinish)
self._sdStreamer.start()
self._comm.startFileTransfer(path, filename[:8].lower() + ".gco")
def deleteSdFile(self, filename):
if not self._comm:
@ -482,13 +462,6 @@ class Printer():
self._sdFile = None
self._comm.deleteSdFile(filename)
def selectSdFile(self, filename, printAfterSelect):
if not self._comm:
return
self._sdPrintAfterSelect = printAfterSelect
self._comm.selectSdFile(filename)
def initSdCard(self):
if not self._comm:
return
@ -504,56 +477,8 @@ class Printer():
return
self._comm.refreshSdFiles()
#~~ callbacks triggered by sdFileStreamer
def _onSdFileStreamProgress(self, filename, progress):
self._stateMonitor.setSdUploadData({"filename": filename, "progress": progress})
def _onSdFileStreamFinish(self, filename):
self._setCurrentZ(None)
self._setProgressData(None, None, None, None)
self._sdStreamer = None
self._stateMonitor.setSdUploadData({"filename": None, "progress": None})
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
#~~ callbacks triggered by gcodeLoader
def _onGcodeLoadingProgress(self, filename, progress, mode):
formattedFilename = None
if filename is not None:
formattedFilename = os.path.basename(filename)
self._stateMonitor.setGcodeData({"filename": formattedFilename, "progress": progress, "mode": mode})
def _onGcodeLoaded(self, filename, gcodeList):
self._setJobData(filename, gcodeList)
self._setCurrentZ(None)
self._setProgressData(None, None, None, None)
self._gcodeLoader = None
self._stateMonitor.setGcodeData({"filename": None, "progress": None})
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
def _onGcodeLoadedToPrint(self, filename, gcodeList):
self._onGcodeLoaded(filename, gcodeList)
self.startPrint()
#~~ state reports
def feedrateState(self):
if self._comm is not None:
feedrateModifiers = self._comm.getFeedrateModifiers()
result = {}
for structure in self._feedrateModifierMapping.keys():
if (feedrateModifiers.has_key(self._feedrateModifierMapping[structure])):
result[structure] = int(round(feedrateModifiers[self._feedrateModifierMapping[structure]] * 100))
else:
result[structure] = 100
return result
else:
return None
def getStateString(self):
"""
Returns a human readable string corresponding to the current communication state.
@ -579,55 +504,7 @@ class Printer():
return self._comm is not None and self._comm.isError()
def isReady(self):
return self._gcodeLoader is None and self._sdStreamer is None and ((self._gcodeList and len(self._gcodeList) > 0) or self._sdFile)
def isLoading(self):
return self._gcodeLoader is not None or self._sdStreamer is not None
class GcodeLoader(threading.Thread):
"""
The GcodeLoader takes care of loading a gcode-File from disk and parsing it into a gcode object in a separate
thread while constantly notifying interested listeners about the current progress.
The progress is returned as a float value between 0 and 1 which is to be interpreted as the percentage of completion.
"""
def __init__(self, filename, progressCallback, loadedCallback):
threading.Thread.__init__(self)
self._progressCallback = progressCallback
self._loadedCallback = loadedCallback
self._filename = filename
self._gcodeList = None
def run(self):
#Send an initial M110 to reset the line counter to zero.
prevLineType = lineType = "CUSTOM"
gcodeList = ["M110 N0"]
filesize = os.stat(self._filename).st_size
with open(self._filename, "r") as file:
for line in file:
if line.startswith(";TYPE:"):
lineType = line[6:].strip()
if ";" in line:
line = line[0:line.find(";")]
line = line.strip()
if len(line) > 0:
if prevLineType != lineType:
gcodeList.append((line, lineType, ))
else:
gcodeList.append(line)
prevLineType = lineType
self._onLoadingProgress(float(file.tell()) / float(filesize))
self._gcodeList = gcodeList
self._loadedCallback(self._filename, self._gcodeList)
def _onLoadingProgress(self, progress):
self._progressCallback(self._filename, progress, "loading")
def _onParsingProgress(self, progress):
self._progressCallback(self._filename, progress, "parsing")
return self.isOperational() and not self._comm.isStreaming()
class SdFileStreamer(threading.Thread):
def __init__(self, comm, filename, file, progressCallback, finishCallback):
@ -644,7 +521,7 @@ class SdFileStreamer(threading.Thread):
return
name = self._filename[:self._filename.rfind(".")]
sdFilename = name[:8] + ".GCO"
sdFilename = name[:8].lower() + ".gco"
try:
size = os.stat(self._file).st_size
with open(self._file, "r") as f:
@ -683,11 +560,9 @@ class StateMonitor(object):
self._worker.daemon = True
self._worker.start()
def reset(self, state=None, jobData=None, gcodeData=None, sdUploadData=None, progress=None, currentZ=None):
def reset(self, state=None, jobData=None, progress=None, currentZ=None):
self.setState(state)
self.setJobData(jobData)
self.setGcodeData(gcodeData)
self.setSdUploadData(sdUploadData)
self.setProgress(progress)
self.setCurrentZ(currentZ)
@ -715,14 +590,6 @@ class StateMonitor(object):
self._jobData = jobData
self._changeEvent.set()
def setGcodeData(self, gcodeData):
self._gcodeData = gcodeData
self._changeEvent.set()
def setSdUploadData(self, uploadData):
self._sdUploadData = uploadData
self._changeEvent.set()
def setProgress(self, progress):
self._progress = progress
self._changeEvent.set()
@ -746,8 +613,6 @@ class StateMonitor(object):
return {
"state": self._state,
"job": self._jobData,
"gcode": self._gcodeData,
"sdUpload": self._sdUploadData,
"currentZ": self._currentZ,
"progress": self._progress
}

View File

@ -232,23 +232,6 @@ def jog():
return jsonify(SUCCESS)
@app.route(BASEURL + "control/speed", methods=["GET"])
def getSpeedValues():
return jsonify(feedrate=printer.feedrateState())
@app.route(BASEURL + "control/speed", methods=["POST"])
@login_required
def speed():
if not printer.isOperational():
return jsonify(SUCCESS)
for key in ["outerWall", "innerWall", "fill", "support"]:
if key in request.values.keys():
value = int(request.values[key])
printer.setFeedrateModifier(key, value)
return getSpeedValues()
@app.route(BASEURL + "control/custom", methods=["GET"])
def getCustomControls():
customControls = settings().get(["controls"])
@ -312,13 +295,14 @@ def loadGcodeFile():
if "print" in request.values.keys() and request.values["print"] in valid_boolean_trues:
printAfterLoading = True
sd = False
filename = None
if "target" in request.values.keys() and request.values["target"] == "sd":
filename = request.values["filename"]
printer.selectSdFile(filename, printAfterLoading)
sd = True
else:
filename = gcodeManager.getAbsolutePath(request.values["filename"])
if filename is not None:
printer.loadGcode(filename, printAfterLoading)
printer.selectFile(filename, sd, printAfterLoading)
return jsonify(SUCCESS)
@app.route(BASEURL + "gcodefiles/delete", methods=["POST"])

View File

@ -525,4 +525,10 @@ ul.dropdown-menu li a {
}
}
}
}
.icon-sd-black-14 {
background: url("../img/icon-sd-black-14.png") 0 3px no-repeat;
width: 11px;
height: 17px;
}

View File

@ -216,21 +216,25 @@ function PrinterStateViewModel(loginStateViewModel) {
self.isSdReady = ko.observable(undefined);
self.filename = ko.observable(undefined);
self.filament = ko.observable(undefined);
self.estimatedPrintTime = ko.observable(undefined);
self.progress = ko.observable(undefined);
self.filesize = ko.observable(undefined);
self.filepos = ko.observable(undefined);
self.printTime = ko.observable(undefined);
self.printTimeLeft = ko.observable(undefined);
self.progress = ko.observable(undefined);
self.currentLine = ko.observable(undefined);
self.totalLines = ko.observable(undefined);
self.sd = ko.observable(undefined);
self.filament = ko.observable(undefined);
self.estimatedPrintTime = ko.observable(undefined);
self.currentHeight = ko.observable(undefined);
self.lineString = ko.computed(function() {
if (!self.totalLines())
self.byteString = ko.computed(function() {
if (!self.filesize())
return "-";
var currentLine = self.currentLine() ? self.currentLine() : "-";
return currentLine + " / " + self.totalLines();
var filepos = self.filepos() ? self.filepos() : "-";
return filepos + " / " + self.filesize();
});
self.progressString = ko.computed(function() {
if (!self.progress())
return 0;
@ -254,8 +258,6 @@ function PrinterStateViewModel(loginStateViewModel) {
self._fromData = function(data) {
self._processStateData(data.state)
self._processJobData(data.job);
self._processGcodeData(data.gcode);
self._processSdUploadData(data.sdUpload);
self._processProgressData(data.progress);
self._processZData(data.currentZ);
}
@ -268,33 +270,15 @@ function PrinterStateViewModel(loginStateViewModel) {
self.isPrinting(data.flags.printing);
self.isError(data.flags.error);
self.isReady(data.flags.ready);
self.isLoading(data.flags.loading);
self.isSdReady(data.flags.sdReady);
}
self._processJobData = function(data) {
self.filename(data.filename);
self.totalLines(data.lines);
self.filesize(data.filesize);
self.estimatedPrintTime(data.estimatedPrintTime);
self.filament(data.filament);
}
self._processGcodeData = function(data) {
if (self.isLoading()) {
var progress = Math.round(data.progress * 100);
if (data.mode == "loading") {
self.filename("Loading... (" + progress + "%)");
} else if (data.mode == "parsing") {
self.filename("Parsing... (" + progress + "%)");
}
}
}
self._processSdUploadData = function(data) {
if (self.isLoading()) {
var progress = Math.round(data.progress * 100);
self.filename("Streaming... (" + progress + "%)");
}
self.sd(data.sd);
}
self._processProgressData = function(data) {
@ -303,7 +287,7 @@ function PrinterStateViewModel(loginStateViewModel) {
} else {
self.progress(undefined);
}
self.currentLine(data.currentLine);
self.filepos(data.filepos);
self.printTime(data.printTime);
self.printTimeLeft(data.printTimeLeft);
}

View File

@ -108,13 +108,13 @@
<div class="accordion-body collapse in" id="state">
<div class="accordion-inner">
Machine State: <strong data-bind="text: stateString"></strong><br>
File: <strong data-bind="text: filename"></strong><br>
File: <strong data-bind="text: filename"></strong>&nbsp;<strong data-bind="visible: sd">(SD)</strong><br>
Filament: <strong data-bind="text: filament"></strong><br>
Estimated Print Time: <strong data-bind="text: estimatedPrintTime"></strong><br>
Line: <strong data-bind="text: lineString"></strong><br>
Height: <strong data-bind="text: currentHeight"></strong><br>
Print Time: <strong data-bind="text: printTime"></strong><br>
Print Time Left: <strong data-bind="text: printTimeLeft"></strong><br>
Printed: <strong data-bind="text: byteString"></strong><br>
<div class="progress">
<div class="bar" id="job_progressBar" data-bind="style: { width: progressString() + '%' }"></div>
@ -153,7 +153,7 @@
{% if enableSdSupport %}
<div class="sd-trigger accordion-heading-button btn-group">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<span class="icon-hdd"></span>
<span class="icon-sd-black-14"></span>
</a>
<ul class="dropdown-menu">
<li data-bind="visible: !isSdReady()"><a href="#" data-bind="click: function() { $root.initSdCard(); }"><i class="icon-flag"></i> Initialize SD card</a></li>

View File

@ -87,6 +87,7 @@ class VirtualPrinter():
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.readList.append("ok")
return
#print "Send: %s" % (data.rstrip())
@ -202,6 +203,7 @@ class VirtualPrinter():
self._writingToSd = True
self._selectedSdFile = file
self.readList.append("Writing to file: %s" % filename)
self.readList.append("ok")
def _finishSdFile(self):
@ -299,16 +301,19 @@ class MachineComPrintCallback(object):
def mcZChange(self, newZ):
pass
def mcFileSelected(self, filename, filesize, sd):
pass
def mcSdStateChange(self, sdReady):
pass
def mcSdFiles(self, files):
pass
def mcSdSelected(self, filename, size):
def mcSdPrintingDone(self):
pass
def mcSdPrintingDone(self):
def mcFileTransferStarted(self, filename, filesize):
pass
class MachineCom(object):
@ -351,15 +356,11 @@ class MachineCom(object):
self._bedTemp = 0
self._targetTemp = 0
self._bedTargetTemp = 0
self._gcodeList = None
self._gcodePos = 0
self._commandQueue = queue.Queue()
self._logQueue = queue.Queue(256)
self._feedRateModifier = {}
self._currentZ = -1
self._heatupWaitStartTime = 0
self._heatupWaitTimeLost = 0.0
self._printStartTime = None
self._alwaysSendChecksum = settings().getBoolean(["feature", "alwaysSendChecksum"])
self._currentLine = 1
@ -373,25 +374,21 @@ class MachineCom(object):
self.thread.daemon = True
self.thread.start()
# SD status data
self._sdAvailable = False
self._sdPrinting = False
self._sdFileList = False
self._sdFile = None
self._sdFilePos = None
self._sdFileSize = None
self._sdFiles = []
# print job
self._currentFile = None
def _changeState(self, newState):
if self._state == newState:
return
if newState == self.STATE_CLOSED or newState == self.STATE_CLOSED_WITH_ERROR:
if settings().get(["feature", "sdSupport"]):
self._sdPrinting = False
self._sdFileList = False
self._sdFile = None
self._sdFilePos = None
self._sdFileSize = None
self._sdFiles = []
self._callback.mcSdFiles([])
@ -417,8 +414,10 @@ class MachineCom(object):
if self._state == self.STATE_OPERATIONAL:
return "Operational"
if self._state == self.STATE_PRINTING:
if self._sdPrinting:
if self.isSdFileSelected():
return "Printing from SD"
elif self.isStreaming():
return "Sending file to SD"
else:
return "Printing"
if self._state == self.STATE_PAUSED:
@ -454,53 +453,51 @@ class MachineCom(object):
return self._state == self.STATE_PRINTING
def isSdPrinting(self):
return self._sdPrinting
return self.isSdFileSelected() and self.isPrinting()
def isSdFileSelected(self):
return self._currentFile is not None and isinstance(self._currentFile, PrintingSdFileInformation)
def isStreaming(self):
return self._currentFile is not None and isinstance(self._currentFile, StreamingGcodeFileInformation)
def isPaused(self):
return self._state == self.STATE_PAUSED
def isBusy(self):
return self.isPrinting() or self._state == self.STATE_RECEIVING_FILE
return self.isPrinting() or self.isPaused()
def isSdReady(self):
return self._sdAvailable
def getPrintPos(self):
if self._sdPrinting:
return self._sdFilePos
else:
return self._gcodePos
def getPrintProgress(self):
if self._currentFile is None:
return None
return self._currentFile.getProgress()
def getPrintFilepos(self):
if self._currentFile is None:
return None
return self._currentFile.getFilepos()
def getPrintTime(self):
if self._printStartTime == None:
return 0
if self._currentFile is None or self._currentFile.getStartTime() is None:
return None
else:
return time.time() - self._printStartTime
return time.time() - self._currentFile.getStartTime()
def getPrintTimeRemainingEstimate(self):
if self._printStartTime == None:
printTime = self.getPrintTime()
if printTime is None:
return None
if self._sdPrinting:
printTime = (time.time() - self._printStartTime) / 60
if self._sdFilePos > 0:
printTimeTotal = printTime * self._sdFileSize / self._sdFilePos
else:
printTimeTotal = printTime * self._sdFileSize
printTimeLeft = printTimeTotal - printTime
return printTimeLeft
printTime /= 60
progress = self._currentFile.getProgress()
if progress:
printTimeTotal = printTime / progress
return printTimeTotal - printTime
else:
# for host printing we only start counting the print time at gcode line 100, so we need to calculate stuff
# a bit different here
if self.getPrintPos() < 200:
return None
printTime = (time.time() - self._printStartTime) / 60
printTimeTotal = printTime * (len(self._gcodeList) - 100) / (self.getPrintPos() - 100)
printTimeLeft = printTimeTotal - printTime
return printTimeLeft
def getSdProgress(self):
return (self._sdFilePos, self._sdFileSize)
return None
def getTemp(self):
return self._temp
@ -593,7 +590,7 @@ class MachineCom(object):
##~~ SD file list
# if we are currently receiving an sd file list, each line is just a filename, so just read it and abort processing
if self._sdFileList and not 'End file list' in line:
self._sdFiles.append(line)
self._sdFiles.append(line.lower())
continue
##~~ Temperature processing
@ -619,6 +616,11 @@ class MachineCom(object):
self._sdAvailable = False
self._sdFiles = []
self._callback.mcSdStateChange(self._sdAvailable)
elif 'Not SD printing' in line:
if self.isSdFileSelected() and self.isPrinting():
# something went wrong, printer is reporting that we actually are not printing right now...
self._sdFilePos = 0
self._changeState(self.STATE_OPERATIONAL)
elif 'SD card ok' in line:
self._sdAvailable = True
self.refreshSdFiles()
@ -632,23 +634,24 @@ class MachineCom(object):
elif 'SD printing byte' in line:
# answer to M27, at least on Marlin, Repetier and Sprinter: "SD printing byte %d/%d"
match = re.search("([0-9]*)/([0-9]*)", line)
self._sdFilePos = int(match.group(1))
self._sdFileSize = int(match.group(2))
self._currentFile.setFilepos(int(match.group(1)))
self._callback.mcProgress()
elif 'File opened' in line:
# answer to M23, at least on Marlin, Repetier and Sprinter: "File opened:%s Size:%d"
match = re.search("File opened:\s*(.*?)\s+Size:\s*([0-9]*)", line)
self._sdFile = match.group(1)
self._sdFileSize = int(match.group(2))
self._currentFile = PrintingSdFileInformation(match.group(1), int(match.group(2)))
elif 'File selected' in line:
# final answer to M23, at least on Marlin, Repetier and Sprinter: "File selected"
self._callback.mcSdSelected(self._sdFile, self._sdFileSize)
self._callback.mcFileSelected(self._currentFile.getFilename(), self._currentFile.getFilesize(), True)
elif 'Writing to file' in line:
# anwer to M28, at least on Marlin, Repetier and Sprinter: "Writing to file: %s"
self._printSection = "CUSTOM"
self._changeState(self.STATE_PRINTING)
elif 'Done printing file' in line:
# printer is reporting file finished printing
self._sdPrinting = False
self._sdFilePos = 0
self._changeState(self.STATE_OPERATIONAL)
self._callback.mcSdPrintingDone()
self._callback.mcPrintjobDone()
##~~ Message handling
elif line.strip() != '' and line.strip() != 'ok' and not line.startswith("wait") and not line.startswith('Resend:') and line != 'echo:Unknown command:""\n' and self.isOperational():
@ -725,7 +728,7 @@ class MachineCom(object):
self._log("Communication timeout during printing, forcing a line")
line = 'ok'
if self._sdPrinting:
if self.isSdPrinting():
if time.time() > tempRequestTimeout:
self._sendCommand("M105")
tempRequestTimeout = time.time() + 5
@ -738,7 +741,7 @@ class MachineCom(object):
timeout = time.time() + 5
else:
# Even when printing request the temperature every 5 seconds.
if time.time() > tempRequestTimeout:
if time.time() > tempRequestTimeout and not self.isStreaming():
self._commandQueue.put("M105")
tempRequestTimeout = time.time() + 5
@ -746,7 +749,7 @@ class MachineCom(object):
timeout = time.time() + 5
if self._resendDelta is not None:
self._resendNextCommand()
elif not self._commandQueue.empty():
elif not self._commandQueue.empty() and not self.isStreaming():
self._sendCommand(self._commandQueue.get())
else:
self._sendNext()
@ -883,7 +886,7 @@ class MachineCom(object):
if self._alwaysSendChecksum:
lineNumber = self._currentLine
else:
lineNumber = self._gcodePos
lineNumber = self._currentFile.getLineCount()
self._addToLastLines(cmd)
self._currentLine += 1
self._doSendWithChecksum(cmd, lineNumber)
@ -916,123 +919,135 @@ class MachineCom(object):
def _sendNext(self):
with self._sendNextLock:
if self._gcodePos >= len(self._gcodeList):
line = self._currentFile.getNext()
if line is None:
if self.isStreaming():
self._sendCommand("M29")
self._currentFile = None
self._callback.mcFileTransferDone()
else:
self._callback.mcPrintjobDone()
self._changeState(self.STATE_OPERATIONAL)
return
if self._gcodePos == 100:
self._printStartTime100 = time.time()
line = self._gcodeList[self._gcodePos]
if type(line) is tuple:
self._printSection = line[1]
line = line[0]
try:
if line == 'M0' or line == 'M1':
self.setPause(True)
line = 'M105' #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
if self._printSection in self._feedRateModifier:
line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line)
if ('G0' in line or 'G1' in line) and 'Z' in line:
z = float(re.search('Z([0-9\.]*)', line).group(1))
if self._currentZ != z:
self._currentZ = z
self._callback.mcZChange(z)
except:
self._log("Unexpected error: %s" % (getExceptionString()))
if not self.isStreaming():
try:
if line == 'M0' or line == 'M1':
self.setPause(True)
line = 'M105' #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
if ('G0' in line or 'G1' in line) and 'Z' in line:
z = float(re.search('Z([0-9\.]*)', line).group(1))
if self._currentZ != z:
self._currentZ = z
self._callback.mcZChange(z)
except:
self._log("Unexpected error: %s" % (getExceptionString()))
self._sendCommand(line, True)
self._gcodePos += 1
self._callback.mcProgress()
def sendCommand(self, cmd):
cmd = cmd.encode('ascii', 'replace')
if self.isPrinting():
if self.isPrinting() and not self.isSdFileSelected():
self._commandQueue.put(cmd)
elif self.isOperational():
self._sendCommand(cmd)
def printGCode(self, gcodeList):
if not self.isOperational() or self.isPrinting():
return
if self._sdPrinting:
self._sdPrinting = False
self._gcodeList = gcodeList
self._gcodePos = 0
self._printSection = 'CUSTOM'
self._changeState(self.STATE_PRINTING)
self._printStartTime = time.time()
self._sendNext()
def printSdFile(self):
def startPrint(self):
if not self.isOperational() or self.isPrinting():
return
if self.isPaused():
self.sendCommand("M26 S0") # reset position in file to byte 0
self.sendCommand("M24")
if self._currentFile is None:
raise ValueError("No file selected for printing")
self._printSection = 'CUSTOM'
self._sdPrinting = True
self._printSection = "CUSTOM"
self._changeState(self.STATE_PRINTING)
self._printStartTime = time.time()
try:
self._currentFile.start()
if self.isSdFileSelected():
if self.isPaused():
self.sendCommand("M26 S0")
self._currentFile.setFilepos(0)
self.sendCommand("M24")
else:
self._sendNext()
except:
self._changeState(self.STATE_ERROR)
self._errorValue = getExceptionString()
def startFileTransfer(self, filename, remoteFilename):
if not self.isOperational() or self.isBusy():
return
self._currentFile = StreamingGcodeFileInformation(filename)
self._currentFile.start()
self.sendCommand("M28 %s" % remoteFilename)
self._callback.mcFileTransferStarted(remoteFilename, self._currentFile.getFilesize())
def selectFile(self, filename, sd):
if self.isBusy():
return
if sd:
if not self.isOperational():
# printer is not connected, can't use SD
return
self.sendCommand("M23 %s" % filename)
else:
self._currentFile = PrintingGcodeFileInformation(filename)
self._callback.mcFileSelected(filename, self._currentFile.getFilesize(), False)
def cancelPrint(self):
if self.isOperational():
self._changeState(self.STATE_OPERATIONAL)
if self._sdPrinting:
self._sdPrinting = False
if not self.isOperational() or self.isStreaming():
return
self._changeState(self.STATE_OPERATIONAL)
if self.isSdFileSelected():
self.sendCommand("M25") # pause print
self.sendCommand("M26 S0") # reset position in file to byte 0
def setPause(self, pause):
if self.isStreaming():
return
if not pause and self.isPaused():
self._changeState(self.STATE_PRINTING)
if self._sdPrinting:
if self.isSdFileSelected():
self.sendCommand("M24")
else:
for i in xrange(0, 6):
self._sendNext()
self._sendNext()
if pause and self.isPrinting():
self._changeState(self.STATE_PAUSED)
if self._sdPrinting:
if self.isSdFileSelected():
self.sendCommand("M25") # pause print
def setFeedrateModifier(self, type, value):
self._feedRateModifier[type] = value
def getFeedrateModifiers(self):
result = {}
result.update(self._feedRateModifier)
return result
##~~ SD card
##~~ SD card handling
def getSdFiles(self):
return self._sdFiles
def startSdFileTransfer(self, filename):
if not self.isOperational() or self.isPrinting() or self.isPaused():
if not self.isOperational() or self.isBusy():
return
self._changeState(self.STATE_RECEIVING_FILE)
self.sendCommand("M28 %s" % filename.lower())
def endSdFileTransfer(self, filename):
if not self.isOperational() or self.isPrinting() or self.isPaused():
if not self.isOperational() or self.isBusy():
return
self.sendCommand("M29 %s" % filename.lower())
self._changeState(self.STATE_OPERATIONAL)
self.refreshSdFiles()
def selectSdFile(self, filename):
if not self.isOperational() or self.isPrinting() or self.isPaused():
return
self._sdFile = None
self._sdFilePos = 0
self.sendCommand("M23 %s" % filename.lower())
def deleteSdFile(self, filename):
if not self.isOperational() or ((self.isPrinting() or self.isPaused()) and self._sdFile == filename.lower()):
if not self.isOperational() or (self.isBusy() and self._sdFile == filename.lower()):
# do not delete a file from sd we are currently printing from
return
@ -1040,7 +1055,7 @@ class MachineCom(object):
self.refreshSdFiles()
def refreshSdFiles(self):
if not self.isOperational() or self.isPrinting() or self.isPaused():
if not self.isOperational() or self.isBusy():
return
self.sendCommand("M20")
@ -1050,7 +1065,7 @@ class MachineCom(object):
self.sendCommand("M21")
def releaseSdCard(self):
if not self.isOperational() or ((self.isPrinting() or self.isPaused()) and self._sdPrinting):
if not self.isOperational() or (self.isBusy() and self.isSdFileSelected()):
# do not release the sd card if we are currently printing from it
return
@ -1064,3 +1079,145 @@ class MachineCom(object):
def getExceptionString():
locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
return "%s: '%s' @ %s:%s:%d" % (str(sys.exc_info()[0].__name__), str(sys.exc_info()[1]), os.path.basename(locationInfo[0]), locationInfo[2], locationInfo[1])
class PrintingFileInformation(object):
"""
Encapsulates information regarding the current file being printed: file name, current position, total size and
time the print started.
Allows to reset the current file position to 0 and to calculate the current progress as a floating point
value between 0 and 1.
"""
def __init__(self, filename):
self._filename = filename
self._filepos = 0
self._filesize = None
self._startTime = None
def getStartTime(self):
return self._startTime
def getFilename(self):
return self._filename
def getFilesize(self):
return self._filesize
def getFilepos(self):
return self._filepos
def getProgress(self):
"""
The current progress of the file, calculated as relation between file position and absolute size. Returns -1
if file size is None or < 1.
"""
if self._filesize is None or not self._filesize > 0:
return -1
return float(self._filepos) / float(self._filesize)
def reset(self):
"""
Resets the current file position to 0.
"""
self._filepos = 0
def start(self):
"""
Marks the print job as started and remembers the start time.
"""
self._startTime = time.time()
class PrintingSdFileInformation(PrintingFileInformation):
"""
Encapsulates information regarding an ongoing print from SD.
"""
def __init__(self, filename, filesize):
PrintingFileInformation.__init__(self, filename)
self._filesize = filesize
def setFilepos(self, filepos):
"""
Sets the current file position.
"""
self._filepos = filepos
class PrintingGcodeFileInformation(PrintingFileInformation):
"""
Encapsulates information regarding an ongoing direct print. Takes care of the needed file handle and ensures
that the file is closed in case of an error.
"""
def __init__(self, filename):
PrintingFileInformation.__init__(self, filename)
self._filehandle = None
self._lineCount = 0
self._firstLine = None
self._prevLineType = None
if not os.path.exists(self._filename) or not os.path.isfile(self._filename):
raise IOError("File %s does not exist" % self._filename)
self._filesize = os.stat(self._filename).st_size
def start(self):
"""
Opens the file for reading and determines the file size. Start time won't be recorded until 100 lines in
"""
self._filehandle = open(self._filename, "r")
self._lineCount = 0
self._prevLineType = "CUSTOM"
def getNext(self):
"""
Retrieves the next line for printing.
"""
if self._filehandle is None:
raise ValueError("File %s is not open for reading" % self._filename)
if self._lineCount == 0:
self._lineCount += 1
return "M110 N0"
try:
processedLine = None
while processedLine is None:
if self._filehandle is None:
# file got closed just now
return None
line = self._filehandle.readline()
self._lineCount += 1
if not line:
self._filehandle.close()
self._filehandle = None
processedLine = self._processLine(line)
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:
self._filehandle.close()
raise e
def getLineCount(self):
return self._lineCount
def _processLine(self, line):
lineType = self._prevLineType
if line.startswith(";TYPE:"):
lineType = line[6:].strip()
if ";" in line:
line = line[0:line.find(";")]
line = line.strip()
if len(line) > 0:
if self._prevLineType != lineType:
return line, lineType
else:
return line
else:
return None
class StreamingGcodeFileInformation(PrintingGcodeFileInformation):
pass