First try at SD card support

master
Gina Häußge 2013-05-20 19:18:03 +02:00
parent fc56744705
commit 039a17d923
8 changed files with 598 additions and 87 deletions

View File

@ -62,6 +62,11 @@ class Printer():
self._filename = None self._filename = None
self._gcodeLoader = None self._gcodeLoader = None
# sd handling
self._sdPrinting = False
self._sdFile = None
self._sdStreamer = None
# feedrate # feedrate
self._feedrateModifierMapping = {"outerWall": "WALL-OUTER", "innerWall": "WALL_INNER", "fill": "FILL", "support": "SUPPORT"} self._feedrateModifierMapping = {"outerWall": "WALL-OUTER", "innerWall": "WALL_INNER", "fill": "FILL", "support": "SUPPORT"}
@ -86,6 +91,7 @@ class Printer():
state={"state": None, "stateString": self.getStateString(), "flags": self._getStateFlags()}, state={"state": None, "stateString": self.getStateString(), "flags": self._getStateFlags()},
jobData={"filename": None, "lines": None, "estimatedPrintTime": None, "filament": None}, jobData={"filename": None, "lines": None, "estimatedPrintTime": None, "filament": None},
gcodeData={"filename": None, "progress": None}, gcodeData={"filename": None, "progress": None},
sdUploadData={"filename": None, "progress": None},
progress={"progress": None, "printTime": None, "printTimeLeft": None}, progress={"progress": None, "printTime": None, "printTimeLeft": None},
currentZ=None currentZ=None
) )
@ -120,7 +126,12 @@ class Printer():
try: callback.sendCurrentData(copy.deepcopy(data)) try: callback.sendCurrentData(copy.deepcopy(data))
except: pass except: pass
#~~ printer commands def _sendTriggerUpdateCallbacks(self, type):
for callback in self._callbacks:
try: callback.sendUpdateTrigger(type)
except: pass
#~~ printer commands
def connect(self, port=None, baudrate=None): def connect(self, port=None, baudrate=None):
""" """
@ -184,13 +195,19 @@ class Printer():
""" """
if self._comm is None or not self._comm.isOperational(): if self._comm is None or not self._comm.isOperational():
return return
if self._gcodeList is None: if self._gcodeList is None and self._sdFile is None:
return return
if self._comm.isPrinting(): if self._comm.isPrinting():
return return
self._setCurrentZ(-1) self._setCurrentZ(-1)
self._comm.printGCode(self._gcodeList) 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)
def togglePausePrint(self): def togglePausePrint(self):
""" """
@ -206,13 +223,17 @@ class Printer():
""" """
if self._comm is None: if self._comm is None:
return return
if self._sdPrinting:
self._sdPrinting = False
self._comm.cancelPrint() self._comm.cancelPrint()
if disableMotorsAndHeater: if disableMotorsAndHeater:
self.commands(["M84", "M104 S0", "M140 S0", "M106 S0"]) # disable motors, switch off heaters and fan self.commands(["M84", "M104 S0", "M140 S0", "M106 S0"]) # disable motors, switch off heaters and fan
# reset line, height, print time # reset line, height, print time
self._setCurrentZ(None) self._setCurrentZ(None)
self._setProgressData(None, None, None) self._setProgressData(None, None, None, None)
# mark print as failure # mark print as failure
self._gcodeManager.printFailed(self._filename) self._gcodeManager.printFailed(self._filename)
@ -250,7 +271,7 @@ class Printer():
self._messages = self._messages[-300:] self._messages = self._messages[-300:]
self._stateMonitor.addMessage(message) self._stateMonitor.addMessage(message)
def _setProgressData(self, progress, printTime, printTimeLeft): def _setProgressData(self, progress, currentLine, printTime, printTimeLeft):
self._progress = progress self._progress = progress
self._printTime = printTime self._printTime = printTime
self._printTimeLeft = printTimeLeft self._printTimeLeft = printTimeLeft
@ -263,7 +284,7 @@ class Printer():
if (self._printTimeLeft): if (self._printTimeLeft):
formattedPrintTimeLeft = util.getFormattedTimeDelta(datetime.timedelta(minutes=self._printTimeLeft)) formattedPrintTimeLeft = util.getFormattedTimeDelta(datetime.timedelta(minutes=self._printTimeLeft))
self._stateMonitor.setProgress({"progress": self._progress, "printTime": formattedPrintTime, "printTimeLeft": formattedPrintTimeLeft}) self._stateMonitor.setProgress({"progress": self._progress, "currentLine": currentLine, "printTime": formattedPrintTime, "printTimeLeft": formattedPrintTimeLeft})
def _addTemperatureData(self, temp, bedTemp, targetTemp, bedTargetTemp): def _addTemperatureData(self, temp, bedTemp, targetTemp, bedTargetTemp):
currentTimeUtc = int(time.time() * 1000) currentTimeUtc = int(time.time() * 1000)
@ -365,13 +386,12 @@ class Printer():
self._gcodeManager.printSucceeded(self._filename) 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: 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) self._gcodeManager.printFailed(self._filename)
self._gcodeManager.resumeAnalysis() # do not analyse gcode while printing self._gcodeManager.resumeAnalysis() # printing done, put those cpu cycles to good use
elif self._comm is not None and state == self._comm.STATE_PRINTING: elif self._comm is not None and state == self._comm.STATE_PRINTING:
self._gcodeManager.pauseAnalysis() # printing done, put those cpu cycles to good use self._gcodeManager.pauseAnalysis() # do not analyse gcode while printing
self._setState(state) self._setState(state)
def mcMessage(self, message): def mcMessage(self, message):
""" """
Callback method for the comm object, called upon message exchanges via serial. Callback method for the comm object, called upon message exchanges via serial.
@ -379,18 +399,22 @@ class Printer():
""" """
self._addMessage(message) self._addMessage(message)
def mcProgress(self, lineNr): def mcProgress(self):
""" """
Callback method for the comm object, called upon any change in progress of the printjob. 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 line.
""" """
oldProgress = self._progress oldProgress = self._progress
if self._timelapse is not None: if self._sdPrinting:
try: self._timelapse.onPrintjobProgress(oldProgress, self._progress, int(round(self._progress * 100 / len(self._gcodeList)))) newLine = None
except: pass (filePos, fileSize) = self._comm.getSdProgress()
newProgress = float(filePos) / float(fileSize)
else:
newLine = self._comm.getPrintPos()
newProgress = float(newLine) / float(len(self._gcodeList))
self._setProgressData(self._comm.getPrintPos(), self._comm.getPrintTime(), self._comm.getPrintTimeRemainingEstimate()) self._setProgressData(newProgress, newLine, self._comm.getPrintTime(), self._comm.getPrintTimeRemainingEstimate())
def mcZChange(self, newZ): def mcZChange(self, newZ):
""" """
@ -402,6 +426,66 @@ class Printer():
self._setCurrentZ(newZ) self._setCurrentZ(newZ)
def mcSdFiles(self, files):
self._sendTriggerUpdateCallbacks("gcodeFiles")
def mcSdSelected(self, filename, filesize):
self._sdFile = filename
self._setJobData(filename, None)
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
if self._sdPrintAfterSelect:
self.startPrint()
def mcSdPrintingDone(self):
self._sdPrinting = False
self._setProgressData(1.0, None, self._comm.getPrintTime(), self._comm.getPrintTimeRemainingEstimate())
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:
return
self._sdStreamer = SdFileStreamer(self._comm, filename, file, self._onSdFileStreamProgress, self._onSdFileStreamFinish)
self._sdStreamer.start()
def deleteSdFile(self, filename):
if not self._comm:
return
if self._sdFile == filename:
self._sdFile = None
self._comm.deleteSdFile(filename)
def selectSdFile(self, filename, printAfterSelect):
if not self._comm:
return
self._sdPrintAfterSelect = printAfterSelect
self._comm.selectSdFile(filename)
#~~ callbacks triggered by sdFileStreamer
def _onSdFileStreamProgress(self, filename, progress):
self._stateMonitor.setSdUploadData({"filename": filename, "progress": progress})
def _onSdFileStreamFinish(self, filename):
self._setJobData(filename, None)
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 #~~ callbacks triggered by gcodeLoader
def _onGcodeLoadingProgress(self, filename, progress, mode): def _onGcodeLoadingProgress(self, filename, progress, mode):
@ -414,7 +498,7 @@ class Printer():
def _onGcodeLoaded(self, filename, gcodeList): def _onGcodeLoaded(self, filename, gcodeList):
self._setJobData(filename, gcodeList) self._setJobData(filename, gcodeList)
self._setCurrentZ(None) self._setCurrentZ(None)
self._setProgressData(None, None, None) self._setProgressData(None, None, None, None)
self._gcodeLoader = None self._gcodeLoader = None
self._stateMonitor.setGcodeData({"filename": None, "progress": None}) self._stateMonitor.setGcodeData({"filename": None, "progress": None})
@ -464,10 +548,10 @@ class Printer():
return self._comm is not None and self._comm.isError() return self._comm is not None and self._comm.isError()
def isReady(self): def isReady(self):
return self._gcodeLoader is None and self._gcodeList and len(self._gcodeList) > 0 return self._gcodeLoader is None and self._sdStreamer is None and ((self._gcodeList and len(self._gcodeList) > 0) or self._sdFile)
def isLoading(self): def isLoading(self):
return self._gcodeLoader is not None return self._gcodeLoader is not None or self._sdStreamer is not None
class GcodeLoader(threading.Thread): class GcodeLoader(threading.Thread):
""" """
@ -514,6 +598,34 @@ class GcodeLoader(threading.Thread):
def _onParsingProgress(self, progress): def _onParsingProgress(self, progress):
self._progressCallback(self._filename, progress, "parsing") self._progressCallback(self._filename, progress, "parsing")
class SdFileStreamer(threading.Thread):
def __init__(self, comm, filename, file, progressCallback, finishCallback):
threading.Thread.__init__(self)
self._comm = comm
self._filename = filename
self._file = file
self._progressCallback = progressCallback
self._finishCallback = finishCallback
def run(self):
if self._comm.isBusy():
return
name = self._filename[:self._filename.rfind(".")]
sdFilename = name[:8] + ".GCO"
try:
size = os.stat(self._file).st_size
with open(self._file, "r") as f:
self._comm.startSdFileTransfer(sdFilename)
for line in f:
self._comm.sendCommand(line)
self._progressCallback(sdFilename, float(f.tell()) / float(size))
time.sleep(0.001) # do not send too fast
finally:
self._comm.endSdFileTransfer(sdFilename)
self._finishCallback(sdFilename)
class StateMonitor(object): class StateMonitor(object):
def __init__(self, ratelimit, updateCallback, addTemperatureCallback, addLogCallback, addMessageCallback): def __init__(self, ratelimit, updateCallback, addTemperatureCallback, addLogCallback, addMessageCallback):
self._ratelimit = ratelimit self._ratelimit = ratelimit
@ -525,6 +637,7 @@ class StateMonitor(object):
self._state = None self._state = None
self._jobData = None self._jobData = None
self._gcodeData = None self._gcodeData = None
self._sdUploadData = None
self._currentZ = None self._currentZ = None
self._progress = None self._progress = None
@ -535,10 +648,11 @@ class StateMonitor(object):
self._worker.daemon = True self._worker.daemon = True
self._worker.start() self._worker.start()
def reset(self, state=None, jobData=None, gcodeData=None, progress=None, currentZ=None): def reset(self, state=None, jobData=None, gcodeData=None, sdUploadData=None, progress=None, currentZ=None):
self.setState(state) self.setState(state)
self.setJobData(jobData) self.setJobData(jobData)
self.setGcodeData(gcodeData) self.setGcodeData(gcodeData)
self.setSdUploadData(sdUploadData)
self.setProgress(progress) self.setProgress(progress)
self.setCurrentZ(currentZ) self.setCurrentZ(currentZ)
@ -570,6 +684,10 @@ class StateMonitor(object):
self._gcodeData = gcodeData self._gcodeData = gcodeData
self._changeEvent.set() self._changeEvent.set()
def setSdUploadData(self, uploadData):
self._sdUploadData = uploadData
self._changeEvent.set()
def setProgress(self, progress): def setProgress(self, progress):
self._progress = progress self._progress = progress
self._changeEvent.set() self._changeEvent.set()
@ -594,6 +712,7 @@ class StateMonitor(object):
"state": self._state, "state": self._state,
"job": self._jobData, "job": self._jobData,
"gcode": self._gcodeData, "gcode": self._gcodeData,
"sdUpload": self._sdUploadData,
"currentZ": self._currentZ, "currentZ": self._currentZ,
"progress": self._progress "progress": self._progress
} }

View File

@ -118,7 +118,8 @@ def index():
enableTimelapse=(settings().get(["webcam", "snapshot"]) is not None and settings().get(["webcam", "ffmpeg"]) is not None), enableTimelapse=(settings().get(["webcam", "snapshot"]) is not None and settings().get(["webcam", "ffmpeg"]) is not None),
enableGCodeVisualizer=settings().get(["feature", "gCodeVisualizer"]), enableGCodeVisualizer=settings().get(["feature", "gCodeVisualizer"]),
enableSystemMenu=settings().get(["system"]) is not None and settings().get(["system", "actions"]) is not None and len(settings().get(["system", "actions"])) > 0, enableSystemMenu=settings().get(["system"]) is not None and settings().get(["system", "actions"]) is not None and len(settings().get(["system", "actions"])) > 0,
enableAccessControl=userManager is not None enableAccessControl=userManager is not None,
enableSdSupport=settings().get(["feature", "sdSupport"])
) )
#~~ Printer control #~~ Printer control
@ -257,7 +258,19 @@ def getCustomControls():
@app.route(BASEURL + "gcodefiles", methods=["GET"]) @app.route(BASEURL + "gcodefiles", methods=["GET"])
def readGcodeFiles(): def readGcodeFiles():
return jsonify(files=gcodeManager.getAllFileData()) files = gcodeManager.getAllFileData()
sdFileList = printer.getSdFiles()
if sdFileList is not None:
for sdFile in sdFileList:
files.append({
"name": sdFile,
"size": "n/a",
"bytes": 0,
"date": "n/a",
"origin": "sd"
})
return jsonify(files=files)
@app.route(BASEURL + "gcodefiles/<path:filename>", methods=["GET"]) @app.route(BASEURL + "gcodefiles/<path:filename>", methods=["GET"])
def readGcodeFile(filename): def readGcodeFile(filename):
@ -270,6 +283,8 @@ def uploadGcodeFile():
if "gcode_file" in request.files.keys(): if "gcode_file" in request.files.keys():
file = request.files["gcode_file"] file = request.files["gcode_file"]
filename = gcodeManager.addFile(file) filename = gcodeManager.addFile(file)
if filename and "target" in request.values.keys() and request.values["target"] == "sd":
printer.addSdFile(filename, gcodeManager.getAbsolutePath(filename))
return jsonify(files=gcodeManager.getAllFileData(), filename=filename) return jsonify(files=gcodeManager.getAllFileData(), filename=filename)
@app.route(BASEURL + "gcodefiles/load", methods=["POST"]) @app.route(BASEURL + "gcodefiles/load", methods=["POST"])
@ -279,9 +294,14 @@ def loadGcodeFile():
printAfterLoading = False printAfterLoading = False
if "print" in request.values.keys() and request.values["print"] in valid_boolean_trues: if "print" in request.values.keys() and request.values["print"] in valid_boolean_trues:
printAfterLoading = True printAfterLoading = True
filename = gcodeManager.getAbsolutePath(request.values["filename"])
if filename is not None: if "target" in request.values.keys() and request.values["target"] == "sd":
printer.loadGcode(filename, printAfterLoading) filename = request.values["filename"]
printer.selectSdFile(filename, printAfterLoading)
else:
filename = gcodeManager.getAbsolutePath(request.values["filename"])
if filename is not None:
printer.loadGcode(filename, printAfterLoading)
return jsonify(SUCCESS) return jsonify(SUCCESS)
@app.route(BASEURL + "gcodefiles/delete", methods=["POST"]) @app.route(BASEURL + "gcodefiles/delete", methods=["POST"])
@ -289,7 +309,10 @@ def loadGcodeFile():
def deleteGcodeFile(): def deleteGcodeFile():
if "filename" in request.values.keys(): if "filename" in request.values.keys():
filename = request.values["filename"] filename = request.values["filename"]
gcodeManager.removeFile(filename) if "target" in request.values.keys() and request.values["target"] == "sd":
printer.deleteSdFile(filename)
else:
gcodeManager.removeFile(filename)
return readGcodeFiles() return readGcodeFiles()
#~~ timelapse handling #~~ timelapse handling
@ -380,7 +403,8 @@ def getSettings():
}, },
"feature": { "feature": {
"gcodeViewer": s.getBoolean(["feature", "gCodeVisualizer"]), "gcodeViewer": s.getBoolean(["feature", "gCodeVisualizer"]),
"waitForStart": s.getBoolean(["feature", "waitForStartOnConnect"]) "waitForStart": s.getBoolean(["feature", "waitForStartOnConnect"]),
"sdSupport": s.getBoolean(["feature", "sdSupport"])
}, },
"folder": { "folder": {
"uploads": s.getBaseFolder("uploads"), "uploads": s.getBaseFolder("uploads"),
@ -424,6 +448,7 @@ def setSettings():
if "feature" in data.keys(): if "feature" in data.keys():
if "gcodeViewer" in data["feature"].keys(): s.setBoolean(["feature", "gCodeVisualizer"], data["feature"]["gcodeViewer"]) if "gcodeViewer" in data["feature"].keys(): s.setBoolean(["feature", "gCodeVisualizer"], data["feature"]["gcodeViewer"])
if "waitForStart" in data["feature"].keys(): s.setBoolean(["feature", "waitForStartOnConnect"], data["feature"]["waitForStart"]) if "waitForStart" in data["feature"].keys(): s.setBoolean(["feature", "waitForStartOnConnect"], data["feature"]["waitForStart"])
if "sdSupport" in data["feature"].keys(): s.setBoolean(["feature", "sdSupport"], data["feature"]["sdSupport"])
if "folder" in data.keys(): if "folder" in data.keys():
if "uploads" in data["folder"].keys(): s.setBaseFolder("uploads", data["folder"]["uploads"]) if "uploads" in data["folder"].keys(): s.setBaseFolder("uploads", data["folder"]["uploads"])

View File

@ -39,13 +39,15 @@ default_settings = {
}, },
"feature": { "feature": {
"gCodeVisualizer": True, "gCodeVisualizer": True,
"waitForStartOnConnect": False "waitForStartOnConnect": False,
"sdSupport": False
}, },
"folder": { "folder": {
"uploads": None, "uploads": None,
"timelapse": None, "timelapse": None,
"timelapse_tmp": None, "timelapse_tmp": None,
"logs": None "logs": None,
"virtualSd": None
}, },
"temperature": { "temperature": {
"profiles": "profiles":

View File

@ -219,6 +219,7 @@ function PrinterStateViewModel(loginStateViewModel) {
self.estimatedPrintTime = ko.observable(undefined); self.estimatedPrintTime = ko.observable(undefined);
self.printTime = ko.observable(undefined); self.printTime = ko.observable(undefined);
self.printTimeLeft = ko.observable(undefined); self.printTimeLeft = ko.observable(undefined);
self.progress = ko.observable(undefined);
self.currentLine = ko.observable(undefined); self.currentLine = ko.observable(undefined);
self.totalLines = ko.observable(undefined); self.totalLines = ko.observable(undefined);
self.currentHeight = ko.observable(undefined); self.currentHeight = ko.observable(undefined);
@ -229,10 +230,10 @@ function PrinterStateViewModel(loginStateViewModel) {
var currentLine = self.currentLine() ? self.currentLine() : "-"; var currentLine = self.currentLine() ? self.currentLine() : "-";
return currentLine + " / " + self.totalLines(); return currentLine + " / " + self.totalLines();
}); });
self.progress = ko.computed(function() { self.progressString = ko.computed(function() {
if (!self.currentLine() || !self.totalLines()) if (!self.progress())
return 0; return 0;
return Math.round(self.currentLine() * 100 / self.totalLines()); return self.progress();
}); });
self.pauseString = ko.computed(function() { self.pauseString = ko.computed(function() {
if (self.isPaused()) if (self.isPaused())
@ -253,6 +254,7 @@ function PrinterStateViewModel(loginStateViewModel) {
self._processStateData(data.state) self._processStateData(data.state)
self._processJobData(data.job); self._processJobData(data.job);
self._processGcodeData(data.gcode); self._processGcodeData(data.gcode);
self._processSdUploadData(data.sdUpload);
self._processProgressData(data.progress); self._processProgressData(data.progress);
self._processZData(data.currentZ); self._processZData(data.currentZ);
} }
@ -286,8 +288,20 @@ function PrinterStateViewModel(loginStateViewModel) {
} }
} }
self._processSdUploadData = function(data) {
if (self.isLoading()) {
var progress = Math.round(data.progress * 100);
self.filename("Streaming to SD... (" + progress + "%)");
}
}
self._processProgressData = function(data) { self._processProgressData = function(data) {
self.currentLine(data.progress); if (data.progress) {
self.progress(Math.round(data.progress * 100));
} else {
self.progress(undefined);
}
self.currentLine(data.currentLine);
self.printTime(data.printTime); self.printTime(data.printTime);
self.printTimeLeft(data.printTimeLeft); self.printTimeLeft(data.printTimeLeft);
} }
@ -754,14 +768,14 @@ function GcodeFilesViewModel(loginStateViewModel) {
return 0; return 0;
}, },
"upload": function(a, b) { "upload": function(a, b) {
// sorts descending // sorts descending
if (a["date"] > b["date"]) return -1; if (b["date"] === undefined || a["date"] > b["date"]) return -1;
if (a["date"] < b["date"]) return 1; if (a["date"] < b["date"]) return 1;
return 0; return 0;
}, },
"size": function(a, b) { "size": function(a, b) {
// sorts descending // sorts descending
if (a["bytes"] > b["bytes"]) return -1; if (b["bytes"] === undefined || a["bytes"] > b["bytes"]) return -1;
if (a["bytes"] < b["bytes"]) return 1; if (a["bytes"] < b["bytes"]) return 1;
return 0; return 0;
} }
@ -769,10 +783,17 @@ function GcodeFilesViewModel(loginStateViewModel) {
{ {
"printed": function(file) { "printed": function(file) {
return !(file["prints"] && file["prints"]["success"] && file["prints"]["success"] > 0); return !(file["prints"] && file["prints"]["success"] && file["prints"]["success"] > 0);
},
"sd": function(file) {
return file["origin"] && file["origin"] == "sd";
},
"local": function(file) {
return !(file["origin"] && file["origin"] == "sd");
} }
}, },
"name", "name",
[], [],
[["sd", "local"]],
CONFIG_GCODEFILESPERPAGE CONFIG_GCODEFILESPERPAGE
); );
@ -823,20 +844,26 @@ function GcodeFilesViewModel(loginStateViewModel) {
} }
self.loadFile = function(filename, printAfterLoad) { self.loadFile = function(filename, printAfterLoad) {
var file = self.listHelper.getItem(function(item) {return item.name == filename});
if (!file) return;
$.ajax({ $.ajax({
url: AJAX_BASEURL + "gcodefiles/load", url: AJAX_BASEURL + "gcodefiles/load",
type: "POST", type: "POST",
dataType: "json", dataType: "json",
data: {filename: filename, print: printAfterLoad} data: {filename: filename, print: printAfterLoad, target: file.origin}
}) })
} }
self.removeFile = function(filename) { self.removeFile = function(filename) {
var file = self.listHelper.getItem(function(item) {return item.name == filename});
if (!file) return;
$.ajax({ $.ajax({
url: AJAX_BASEURL + "gcodefiles/delete", url: AJAX_BASEURL + "gcodefiles/delete",
type: "POST", type: "POST",
dataType: "json", dataType: "json",
data: {filename: filename}, data: {filename: filename, target: file.origin},
success: self.fromResponse success: self.fromResponse
}) })
} }
@ -917,6 +944,7 @@ function TimelapseViewModel(loginStateViewModel) {
}, },
"name", "name",
[], [],
[],
CONFIG_TIMELAPSEFILESPERPAGE CONFIG_TIMELAPSEFILESPERPAGE
) )
@ -996,13 +1024,15 @@ function GcodeViewModel(loginStateViewModel) {
self.status = 'idle'; self.status = 'idle';
self.enabled = false; self.enabled = false;
self.errorCount = 0;
self.initialize = function(){ self.initialize = function(){
self.enabled = true; self.enabled = true;
GCODE.ui.initHandlers(); GCODE.ui.initHandlers();
} }
self.loadFile = function(filename){ self.loadFile = function(filename){
if (self.status == 'idle') { if (self.status == 'idle' && self.errorCount < 3) {
self.status = 'request'; self.status = 'request';
$.ajax({ $.ajax({
url: AJAX_BASEURL + "gcodefiles/" + filename, url: AJAX_BASEURL + "gcodefiles/" + filename,
@ -1016,6 +1046,7 @@ function GcodeViewModel(loginStateViewModel) {
}, },
error: function() { error: function() {
self.status = 'idle'; self.status = 'idle';
self.errorCount++;
} }
}) })
} }
@ -1045,6 +1076,7 @@ function GcodeViewModel(loginStateViewModel) {
GCODE.renderer.render(cmdIndex.layer, 0, cmdIndex.cmd); GCODE.renderer.render(cmdIndex.layer, 0, cmdIndex.cmd);
GCODE.ui.updateLayerInfo(cmdIndex.layer); GCODE.ui.updateLayerInfo(cmdIndex.layer);
} }
self.errorCount = 0
} else if (data.job.filename) { } else if (data.job.filename) {
self.loadFile(data.job.filename); self.loadFile(data.job.filename);
} }
@ -1198,7 +1230,7 @@ function UsersViewModel(loginStateViewModel) {
if (user === undefined) return; if (user === undefined) return;
if (user.name == loginStateViewModel.username()) { if (user.name == loginStateViewModel.username()) {
// we do not allow to delete ourself // we do not allow to delete ourselves
$.pnotify({title: "Not possible", text: "You may not delete your own account.", type: "error"}); $.pnotify({title: "Not possible", text: "You may not delete your own account.", type: "error"});
return; return;
} }
@ -1267,6 +1299,7 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) {
self.feature_gcodeViewer = ko.observable(undefined); self.feature_gcodeViewer = ko.observable(undefined);
self.feature_waitForStart = ko.observable(undefined); self.feature_waitForStart = ko.observable(undefined);
self.feature_sdSupport = ko.observable(undefined);
self.folder_uploads = ko.observable(undefined); self.folder_uploads = ko.observable(undefined);
self.folder_timelapse = ko.observable(undefined); self.folder_timelapse = ko.observable(undefined);
@ -1311,6 +1344,7 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) {
self.feature_gcodeViewer(response.feature.gcodeViewer); self.feature_gcodeViewer(response.feature.gcodeViewer);
self.feature_waitForStart(response.feature.waitForStart); self.feature_waitForStart(response.feature.waitForStart);
self.feature_sdSupport(response.feature.sdSupport);
self.folder_uploads(response.folder.uploads); self.folder_uploads(response.folder.uploads);
self.folder_timelapse(response.folder.timelapse); self.folder_timelapse(response.folder.timelapse);
@ -1343,7 +1377,8 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) {
}, },
"feature": { "feature": {
"gcodeViewer": self.feature_gcodeViewer(), "gcodeViewer": self.feature_gcodeViewer(),
"waitForStart": self.feature_waitForStart() "waitForStart": self.feature_waitForStart(),
"sdSupport": self.feature_sdSupport()
}, },
"folder": { "folder": {
"uploads": self.folder_uploads(), "uploads": self.folder_uploads(),
@ -1475,7 +1510,7 @@ function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewM
} }
} }
function ItemListHelper(listType, supportedSorting, supportedFilters, defaultSorting, defaultFilters, filesPerPage) { function ItemListHelper(listType, supportedSorting, supportedFilters, defaultSorting, defaultFilters, exclusiveFilters, filesPerPage) {
var self = this; var self = this;
self.listType = listType; self.listType = listType;
@ -1483,6 +1518,7 @@ function ItemListHelper(listType, supportedSorting, supportedFilters, defaultSor
self.supportedFilters = supportedFilters; self.supportedFilters = supportedFilters;
self.defaultSorting = defaultSorting; self.defaultSorting = defaultSorting;
self.defaultFilters = defaultFilters; self.defaultFilters = defaultFilters;
self.exclusiveFilters = exclusiveFilters;
self.allItems = []; self.allItems = [];
self.items = ko.observableArray([]); self.items = ko.observableArray([]);
@ -1574,6 +1610,17 @@ function ItemListHelper(listType, supportedSorting, supportedFilters, defaultSor
} }
} }
self.getItem = function(matcher) {
var itemList = self.items();
for (var i = 0; i < itemList.length; i++) {
if (matcher(itemList[i])) {
return itemList[i];
}
}
return undefined;
}
//~~ sorting //~~ sorting
self.changeSorting = function(sorting) { self.changeSorting = function(sorting) {
@ -1604,6 +1651,16 @@ function ItemListHelper(listType, supportedSorting, supportedFilters, defaultSor
if (!_.contains(_.keys(self.supportedFilters), filter)) if (!_.contains(_.keys(self.supportedFilters), filter))
return; return;
for (var i = 0; i < self.exclusiveFilters.length; i++) {
if (_.contains(self.exclusiveFilters[i], filter)) {
for (var j = 0; j < self.exclusiveFilters[i].length; j++) {
if (self.exclusiveFilters[i][j] == filter)
continue;
self.removeFilter(self.exclusiveFilters[i][j]);
}
}
}
var filters = self.currentFilters(); var filters = self.currentFilters();
filters.push(filter); filters.push(filter);
self.currentFilters(filters); self.currentFilters(filters);
@ -1613,7 +1670,7 @@ function ItemListHelper(listType, supportedSorting, supportedFilters, defaultSor
} }
self.removeFilter = function(filter) { self.removeFilter = function(filter) {
if (filter != "printed") if (!_.contains(_.keys(self.supportedFilters), filter))
return; return;
var filters = self.currentFilters(); var filters = self.currentFilters();
@ -1847,6 +1904,26 @@ $(function() {
} }
}); });
$("#gcode_upload_sd").fileupload({
dataType: "json",
formData: {target: "sd"},
done: function (e, data) {
gcodeFilesViewModel.fromResponse(data.result);
$("#gcode_upload_progress .bar").css("width", "0%");
$("#gcode_upload_progress").removeClass("progress-striped").removeClass("active");
$("#gcode_upload_progress .bar").text("");
},
progressall: function (e, data) {
var progress = parseInt(data.loaded / data.total * 100, 10);
$("#gcode_upload_progress .bar").css("width", progress + "%");
$("#gcode_upload_progress .bar").text("Uploading ...");
if (progress >= 100) {
$("#gcode_upload_progress").addClass("progress-striped").addClass("active");
$("#gcode_upload_progress .bar").text("Saving ...");
}
}
});
//~~ Offline overlay //~~ Offline overlay
$("#offline_overlay_reconnect").click(function() {dataUpdater.reconnect()}); $("#offline_overlay_reconnect").click(function() {dataUpdater.reconnect()});
@ -1908,7 +1985,7 @@ $(function() {
} else { } else {
$("#gcode_upload").fileupload("disable"); $("#gcode_upload").fileupload("disable");
} }
}) });
//~~ UI stuff //~~ UI stuff

View File

@ -24,6 +24,7 @@
var CONFIG_USERSPERPAGE = 10; var CONFIG_USERSPERPAGE = 10;
var CONFIG_WEBCAM_STREAM = "{{ webcamStream }}"; var CONFIG_WEBCAM_STREAM = "{{ webcamStream }}";
var CONFIG_ACCESS_CONTROL = {% if enableAccessControl -%} true; {% else %} false; {%- endif %} var CONFIG_ACCESS_CONTROL = {% if enableAccessControl -%} true; {% else %} false; {%- endif %}
var CONFIG_SD_SUPPORT = {% if enableSdSupport -%} true; {% else %} false; {%- endif %}
var WEB_SOCKET_SWF_LOCATION = "{{ url_for('static', filename='js/socket.io/WebSocketMain.swf') }}"; var WEB_SOCKET_SWF_LOCATION = "{{ url_for('static', filename='js/socket.io/WebSocketMain.swf') }}";
var WEB_SOCKET_DEBUG = true; var WEB_SOCKET_DEBUG = true;
@ -116,7 +117,7 @@
Print Time Left: <strong data-bind="text: printTimeLeft"></strong><br> Print Time Left: <strong data-bind="text: printTimeLeft"></strong><br>
<div class="progress"> <div class="progress">
<div class="bar" id="job_progressBar" data-bind="style: { width: progress() + '%' }"></div> <div class="bar" id="job_progressBar" data-bind="style: { width: progressString() + '%' }"></div>
</div> </div>
<div class="row-fluid print-control" style="display: none;" data-bind="visible: loginState.isUser"> <div class="row-fluid print-control" style="display: none;" data-bind="visible: loginState.isUser">
@ -139,6 +140,11 @@
<li><a href="#" data-bind="click: function() { $root.listHelper.changeSorting('name'); }"><i class="icon-ok" data-bind="style: {visibility: listHelper.currentSorting() == 'name' ? 'visible' : 'hidden'}"></i> Sort by name (ascending)</a></li> <li><a href="#" data-bind="click: function() { $root.listHelper.changeSorting('name'); }"><i class="icon-ok" data-bind="style: {visibility: listHelper.currentSorting() == 'name' ? 'visible' : 'hidden'}"></i> Sort by name (ascending)</a></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.changeSorting('upload'); }"><i class="icon-ok" data-bind="style: {visibility: listHelper.currentSorting() == 'upload' ? 'visible' : 'hidden'}"></i> Sort by upload date (descending)</a></li> <li><a href="#" data-bind="click: function() { $root.listHelper.changeSorting('upload'); }"><i class="icon-ok" data-bind="style: {visibility: listHelper.currentSorting() == 'upload' ? 'visible' : 'hidden'}"></i> Sort by upload date (descending)</a></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.changeSorting('size'); }"><i class="icon-ok" data-bind="style: {visibility: listHelper.currentSorting() == 'size' ? 'visible' : 'hidden'}"></i> Sort by file size (descending)</a></li> <li><a href="#" data-bind="click: function() { $root.listHelper.changeSorting('size'); }"><i class="icon-ok" data-bind="style: {visibility: listHelper.currentSorting() == 'size' ? 'visible' : 'hidden'}"></i> Sort by file size (descending)</a></li>
{% if enableSdSupport %}
<li class="divider"></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.toggleFilter('local'); }"><i class="icon-ok" data-bind="style: {visibility: _.contains(listHelper.currentFilters(), 'local') ? 'visible' : 'hidden'}"></i> Only show files stored locally</a></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.toggleFilter('sd'); }"><i class="icon-ok" data-bind="style: {visibility: _.contains(listHelper.currentFilters(), 'sd') ? 'visible' : 'hidden'}"></i> Only show files stored on SD</a></li>
{% endif %}
<li class="divider"></li> <li class="divider"></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.toggleFilter('printed'); }"><i class="icon-ok" data-bind="style: {visibility: _.contains(listHelper.currentFilters(), 'printed') ? 'visible' : 'hidden'}"></i> Hide successfully printed files</a></li> <li><a href="#" data-bind="click: function() { $root.listHelper.toggleFilter('printed'); }"><i class="icon-ok" data-bind="style: {visibility: _.contains(listHelper.currentFilters(), 'printed') ? 'visible' : 'hidden'}"></i> Hide successfully printed files</a></li>
</ul> </ul>
@ -181,6 +187,11 @@
<span>Upload</span> <span>Upload</span>
<input id="gcode_upload" type="file" name="gcode_file" class="fileinput-button" data-url="/ajax/gcodefiles/upload" data-bind="enable: loginState.isUser()"> <input id="gcode_upload" type="file" name="gcode_file" class="fileinput-button" data-url="/ajax/gcodefiles/upload" data-bind="enable: loginState.isUser()">
</span> </span>
<span class="btn btn-primary btn-block fileinput-button" data-bind="css: {disabled: !$root.loginState.isUser()}" style="margin-bottom: 10px">
<i class="icon-upload icon-white"></i>
<span>Upload to SD</span>
<input id="gcode_upload_sd" type="file" name="gcode_file" class="fileinput-button" data-url="/ajax/gcodefiles/upload" data-bind="enable: loginState.isUser()">
</span>
<div id="gcode_upload_progress" class="progress" style="width: 100%;"> <div id="gcode_upload_progress" class="progress" style="width: 100%;">
<div class="bar" style="width: 0%"></div> <div class="bar" style="width: 0%"></div>
</div> </div>

View File

@ -107,6 +107,13 @@
</label> </label>
</div> </div>
</div> </div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: feature_sdSupport" id="settings-featureSdSupport"> Enable SD support
</label>
</div>
</div>
</form> </form>
</div> </div>
<div class="tab-pane" id="settings_folder"> <div class="tab-pane" id="settings_folder">

View File

@ -53,9 +53,6 @@ class Timelapse(object):
def onPrintjobStopped(self): def onPrintjobStopped(self):
self.stopTimelapse() self.stopTimelapse()
def onPrintjobProgress(self, oldPos, newPos, percentage):
pass
def onZChange(self, oldZ, newZ): def onZChange(self, oldZ, newZ):
pass pass

View File

@ -64,9 +64,24 @@ class VirtualPrinter():
self.bedTemp = 1.0 self.bedTemp = 1.0
self.bedTargetTemp = 1.0 self.bedTargetTemp = 1.0
self._virtualSd = settings().getBaseFolder("virtualSd")
self._sdPrinter = None
self._sdPrintingSemaphore = threading.Event()
self._selectedSdFile = None
self._selectedSdFileSize = None
self._selectedSdFilePos = None
self._writingToSd = False
def write(self, data): def write(self, data):
if self.readList is None: if self.readList is None:
return 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)
return
#print "Send: %s" % (data.rstrip()) #print "Send: %s" % (data.rstrip())
if 'M104' in data or 'M109' in data: if 'M104' in data or 'M109' in data:
try: try:
@ -78,11 +93,121 @@ class VirtualPrinter():
self.bedTargetTemp = float(re.search('S([0-9]+)', data).group(1)) self.bedTargetTemp = float(re.search('S([0-9]+)', data).group(1))
except: except:
pass pass
if 'M105' in data: if 'M105' in data:
self.readList.append("ok T:%.2f /%.2f B:%.2f /%.2f @:64\n" % (self.temp, self.targetTemp, self.bedTemp, self.bedTargetTemp)) self.readList.append("ok T:%.2f /%.2f B:%.2f /%.2f @:64\n" % (self.temp, self.targetTemp, self.bedTemp, self.bedTargetTemp))
elif 'M20' in data:
self._listSd()
elif 'M23' in data:
filename = data.split(None, 1)[1].strip()
self._selectSdFile(filename)
elif 'M24' in data:
self._startSdPrint()
elif 'M25' in data:
self._pauseSdPrint()
elif 'M27' in data:
self._reportSdStatus()
elif 'M28' in data:
filename = data.split(None, 1)[1].strip()
self._writeSdFile(filename)
elif 'M29' in data:
self._finishSdFile()
elif 'M30' in data:
filename = data.split(None, 1)[1].strip()
self._deleteSdFile(filename)
elif len(data.strip()) > 0: elif len(data.strip()) > 0:
self.readList.append("ok\n") self.readList.append("ok\n")
def _listSd(self):
self.readList.append("Begin file list")
for osFile in os.listdir(self._virtualSd):
self.readList.append(osFile.upper())
self.readList.append("End file list")
self.readList.append("ok")
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)
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")
def _startSdPrint(self):
if self._selectedSdFile is not None:
if self._sdPrinter is None:
self._sdPrinter = threading.Thread(target=self._sdPrintingWorker)
self._sdPrinter.start()
self._sdPrintingSemaphore.set()
self.readList.append("ok")
def _pauseSdPrint(self):
self._sdPrintingSemaphore.clear()
self.readList.append("ok")
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))
else:
self.readList.append("Not SD printing")
def _writeSdFile(self, filename):
file = os.path.join(self._virtualSd, filename).lower()
if os.path.exists(file):
if os.path.isfile(file):
os.remove(file)
else:
self.readList.append("error writing to file")
self._writingToSd = True
self._selectedSdFile = file
self.readList.append("ok")
def _finishSdFile(self):
self._writingToSd = False
self._selectedSdFile = None
self.readList.append("ok")
def _sdPrintingWorker(self):
self._selectedSdFilePos = 0
with open(self._selectedSdFile, "rb") as f:
for line in f:
# read current file position
print("Progress: %d (%d / %d)" % ((round(self._selectedSdFilePos * 100 / self._selectedSdFileSize)), self._selectedSdFilePos, self._selectedSdFileSize))
self._selectedSdFilePos = f.tell()
# if we are paused, wait for unpausing
self._sdPrintingSemaphore.wait()
# set target temps
if 'M104' in line or 'M109' in line:
try:
self.targetTemp = float(re.search('S([0-9]+)', line).group(1))
except:
pass
if 'M140' in line or 'M190' in line:
try:
self.bedTargetTemp = float(re.search('S([0-9]+)', line).group(1))
except:
pass
print line
time.sleep(0.01)
self._sdPrintingSemaphore.clear()
self._selectedSdFilePos = 0
self._sdPrinter = None
self.readList.append("Done printing file")
def _deleteSdFile(self, filename):
file = os.path.join(self._virtualSd, filename)
if os.path.exists(file) and os.path.isfile(file):
os.remove(file)
self.readList.append("ok")
def readline(self): def readline(self):
if self.readList is None: if self.readList is None:
return '' return ''
@ -105,7 +230,6 @@ class VirtualPrinter():
if self.readList is None: if self.readList is None:
return '' return ''
time.sleep(0.001) time.sleep(0.001)
#print "Recv: %s" % (self.readList[0].rstrip())
return self.readList.pop(0) return self.readList.pop(0)
def close(self): def close(self):
@ -124,12 +248,21 @@ class MachineComPrintCallback(object):
def mcMessage(self, message): def mcMessage(self, message):
pass pass
def mcProgress(self, lineNr): def mcProgress(self):
pass pass
def mcZChange(self, newZ): def mcZChange(self, newZ):
pass pass
def mcSdFiles(self, files):
pass
def mcSdSelected(self, filename, size):
pass
def mcSdPrintingDone(self):
pass
class MachineCom(object): class MachineCom(object):
STATE_NONE = 0 STATE_NONE = 0
STATE_OPEN_SERIAL = 1 STATE_OPEN_SERIAL = 1
@ -142,6 +275,7 @@ class MachineCom(object):
STATE_CLOSED = 8 STATE_CLOSED = 8
STATE_ERROR = 9 STATE_ERROR = 9
STATE_CLOSED_WITH_ERROR = 10 STATE_CLOSED_WITH_ERROR = 10
STATE_RECEIVING_FILE = 11
def __init__(self, port = None, baudrate = None, callbackObject = None): def __init__(self, port = None, baudrate = None, callbackObject = None):
self._logger = logging.getLogger(__name__) self._logger = logging.getLogger(__name__)
@ -177,12 +311,19 @@ class MachineCom(object):
self._currentZ = -1 self._currentZ = -1
self._heatupWaitStartTime = 0 self._heatupWaitStartTime = 0
self._heatupWaitTimeLost = 0.0 self._heatupWaitTimeLost = 0.0
self._printStartTime100 = None self._printStartTime = None
self.thread = threading.Thread(target=self._monitor) self.thread = threading.Thread(target=self._monitor)
self.thread.daemon = True self.thread.daemon = True
self.thread.start() self.thread.start()
self._sdPrinting = False
self._sdFileList = False
self._sdFile = None
self._sdFilePos = None
self._sdFileSize = None
self._sdFiles = []
def _changeState(self, newState): def _changeState(self, newState):
if self._state == newState: if self._state == newState:
return return
@ -208,7 +349,10 @@ class MachineCom(object):
if self._state == self.STATE_OPERATIONAL: if self._state == self.STATE_OPERATIONAL:
return "Operational" return "Operational"
if self._state == self.STATE_PRINTING: if self._state == self.STATE_PRINTING:
return "Printing" if self._sdPrinting:
return "Printing from SD"
else:
return "Printing"
if self._state == self.STATE_PAUSED: if self._state == self.STATE_PAUSED:
return "Paused" return "Paused"
if self._state == self.STATE_CLOSED: if self._state == self.STATE_CLOSED:
@ -217,6 +361,8 @@ class MachineCom(object):
return "Error: %s" % (self.getShortErrorString()) return "Error: %s" % (self.getShortErrorString())
if self._state == self.STATE_CLOSED_WITH_ERROR: if self._state == self.STATE_CLOSED_WITH_ERROR:
return "Error: %s" % (self.getShortErrorString()) return "Error: %s" % (self.getShortErrorString())
if self._state == self.STATE_RECEIVING_FILE:
return "Streaming file to SD"
return "?%d?" % (self._state) return "?%d?" % (self._state)
def getShortErrorString(self): def getShortErrorString(self):
@ -234,31 +380,57 @@ class MachineCom(object):
return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR
def isOperational(self): def isOperational(self):
return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED or self._state == self.STATE_RECEIVING_FILE
def isPrinting(self): def isPrinting(self):
return self._state == self.STATE_PRINTING return self._state == self.STATE_PRINTING
def isSdPrinting(self):
return self._sdPrinting
def isPaused(self): def isPaused(self):
return self._state == self.STATE_PAUSED return self._state == self.STATE_PAUSED
def isBusy(self):
return self.isPrinting() or self._state == self.STATE_RECEIVING_FILE
def getPrintPos(self): def getPrintPos(self):
return self._gcodePos if self._sdPrinting:
return self._sdFilePos
else:
return self._gcodePos
def getPrintTime(self): def getPrintTime(self):
if self._printStartTime100 == None: if self._printStartTime == None:
return 0 return 0
else: else:
return time.time() - self._printStartTime100 return time.time() - self._printStartTime
def getPrintTimeRemainingEstimate(self): def getPrintTimeRemainingEstimate(self):
if self._printStartTime100 == None or self.getPrintPos() < 200: if self._printStartTime == None:
return None return None
printTime = (time.time() - self._printStartTime100) / 60
printTimeTotal = printTime * (len(self._gcodeList) - 100) / (self.getPrintPos() - 100) if self._sdPrinting:
printTimeLeft = printTimeTotal - printTime printTime = (time.time() - self._printStartTime) / 60
return printTimeLeft if self._sdFilePos > 0:
printTimeTotal = printTime * (self._sdFileSize / self._sdFilePos)
else:
printTimeTotal = printTime * self._sdFileSize
printTimeLeft = printTimeTotal - printTime
return printTimeLeft
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)
def getTemp(self): def getTemp(self):
return self._temp return self._temp
@ -318,13 +490,15 @@ class MachineCom(object):
#Start monitoring the serial port. #Start monitoring the serial port.
timeout = time.time() + 5 timeout = time.time() + 5
tempRequestTimeout = timeout tempRequestTimeout = timeout
sdStatusRequestTimeout = timeout
startSeen = not settings().getBoolean(["feature", "waitForStartOnConnect"]) startSeen = not settings().getBoolean(["feature", "waitForStartOnConnect"])
while True: while True:
line = self._readline() line = self._readline()
if line == None: if line == None:
break break
#No matter the state, if we see an error, goto the error state and store the error for reference. ##~~ Error handling
# No matter the state, if we see an error, goto the error state and store the error for reference.
if line.startswith('Error:'): if line.startswith('Error:'):
#Oh YEAH, consistency. #Oh YEAH, consistency.
# Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n" # Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n"
@ -338,6 +512,14 @@ class MachineCom(object):
elif not self.isError(): elif not self.isError():
self._errorValue = line[6:] self._errorValue = line[6:]
self._changeState(self.STATE_ERROR) self._changeState(self.STATE_ERROR)
##~~ 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)
continue
##~~ Temperature processing
if ' T:' in line or line.startswith('T:'): if ' T:' in line or line.startswith('T:'):
self._temp = float(re.search("-?[0-9\.]*", line.split('T:')[1]).group(0)) self._temp = float(re.search("-?[0-9\.]*", line.split('T:')[1]).group(0))
if ' B:' in line: if ' B:' in line:
@ -348,6 +530,36 @@ class MachineCom(object):
t = time.time() t = time.time()
self._heatupWaitTimeLost = t - self._heatupWaitStartTime self._heatupWaitTimeLost = t - self._heatupWaitStartTime
self._heatupWaitStartTime = t self._heatupWaitStartTime = t
##~~ SD Card handling
elif 'Begin file list' in line:
self._sdFiles = []
self._sdFileList = True
elif 'End file list' in line:
self._sdFileList = False
self._callback.mcSdFiles(self._sdFiles)
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._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: (.*?) Size: ([0-9]*)", line)
self._sdFile = match.group(1)
self._sdFileSize = 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)
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()
##~~ Message handling
elif line.strip() != '' and line.strip() != 'ok' and not line.startswith('Resend:') and line != 'echo:Unknown command:""\n' and self.isOperational(): elif line.strip() != '' and line.strip() != 'ok' and not line.startswith('Resend:') and line != 'echo:Unknown command:""\n' and self.isOperational():
self._callback.mcMessage(line) self._callback.mcMessage(line)
@ -395,6 +607,8 @@ class MachineCom(object):
startSeen = True startSeen = True
elif 'ok' in line and startSeen: elif 'ok' in line and startSeen:
self._changeState(self.STATE_OPERATIONAL) self._changeState(self.STATE_OPERATIONAL)
if settings().get(["feature", "sdSupport"]):
self._sendCommand("M20")
elif time.time() > timeout: elif time.time() > timeout:
self.close() self.close()
elif self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PAUSED: elif self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PAUSED:
@ -403,25 +617,35 @@ class MachineCom(object):
self._sendCommand("M105") self._sendCommand("M105")
tempRequestTimeout = time.time() + 5 tempRequestTimeout = time.time() + 5
elif self._state == self.STATE_PRINTING: elif self._state == self.STATE_PRINTING:
if line == '' and time.time() > timeout: # Even when printing request the temperture every 5 seconds.
self._log("Communication timeout during printing, forcing a line")
line = 'ok'
#Even when printing request the temperture every 5 seconds.
if time.time() > tempRequestTimeout: if time.time() > tempRequestTimeout:
self._commandQueue.put("M105") self._commandQueue.put("M105")
tempRequestTimeout = time.time() + 5 tempRequestTimeout = time.time() + 5
if 'ok' in line:
timeout = time.time() + 5 if self._sdPrinting:
if time.time() > sdStatusRequestTimeout:
self._commandQueue.put("M27")
sdStatusRequestTimeout = time.time() + 1
if not self._commandQueue.empty(): if not self._commandQueue.empty():
self._sendCommand(self._commandQueue.get()) self._sendCommand(self._commandQueue.get())
else: else:
self._sendNext() if line == '' and time.time() > timeout:
elif "resend" in line.lower() or "rs" in line: self._log("Communication timeout during printing, forcing a line")
try: line = 'ok'
self._gcodePos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
except: if 'ok' in line:
if "rs" in line: timeout = time.time() + 5
self._gcodePos = int(line.split()[1]) if not self._commandQueue.empty():
self._sendCommand(self._commandQueue.get())
else:
self._sendNext()
elif "resend" in line.lower() or "rs" in line:
try:
self._gcodePos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
except:
if "rs" in line:
self._gcodePos = int(line.split()[1])
self._log("Connection closed, closing down monitor") self._log("Connection closed, closing down monitor")
def _log(self, message): def _log(self, message):
@ -501,7 +725,7 @@ class MachineCom(object):
self._changeState(self.STATE_OPERATIONAL) self._changeState(self.STATE_OPERATIONAL)
return return
if self._gcodePos == 100: if self._gcodePos == 100:
self._printStartTime100 = time.time() self._printStartTime = time.time()
line = self._gcodeList[self._gcodePos] line = self._gcodeList[self._gcodePos]
if type(line) is tuple: if type(line) is tuple:
self._printSection = line[1] self._printSection = line[1]
@ -522,7 +746,7 @@ class MachineCom(object):
checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line))) checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line)))
self._sendCommand("N%d%s*%d" % (self._gcodePos, line, checksum)) self._sendCommand("N%d%s*%d" % (self._gcodePos, line, checksum))
self._gcodePos += 1 self._gcodePos += 1
self._callback.mcProgress(self._gcodePos) self._callback.mcProgress()
def sendCommand(self, cmd): def sendCommand(self, cmd):
cmd = cmd.encode('ascii', 'replace') cmd = cmd.encode('ascii', 'replace')
@ -536,25 +760,50 @@ class MachineCom(object):
return return
self._gcodeList = gcodeList self._gcodeList = gcodeList
self._gcodePos = 0 self._gcodePos = 0
self._printStartTime100 = None
self._printSection = 'CUSTOM' self._printSection = 'CUSTOM'
self._changeState(self.STATE_PRINTING) self._changeState(self.STATE_PRINTING)
self._printStartTime = time.time() self._printStartTime = time.time()
for i in xrange(0, 6): for i in xrange(0, 6):
self._sendNext() self._sendNext()
def selectSdFile(self, filename):
if not self.isOperational() or self.isPrinting():
return
self._sdFile = None
self._sdFilePos = 0
self.sendCommand("M23 %s" % filename)
def printSdFile(self):
if not self.isOperational() or self.isPrinting():
return
self.sendCommand("M24")
self._printSection = 'CUSTOM'
self._sdPrinting = True
self._changeState(self.STATE_PRINTING)
self._printStartTime = time.time()
def cancelPrint(self): def cancelPrint(self):
if self.isOperational(): if self.isOperational():
self._changeState(self.STATE_OPERATIONAL) self._changeState(self.STATE_OPERATIONAL)
if self._sdPrinting:
self.sendCommand("M25")
def setPause(self, pause): def setPause(self, pause):
if not pause and self.isPaused(): if not pause and self.isPaused():
self._changeState(self.STATE_PRINTING) self._changeState(self.STATE_PRINTING)
for i in xrange(0, 6): if self._sdPrinting:
self._sendNext() self.sendCommand("M24")
else:
for i in xrange(0, 6):
self._sendNext()
if pause and self.isPrinting(): if pause and self.isPrinting():
self._changeState(self.STATE_PAUSED) self._changeState(self.STATE_PAUSED)
if self._sdPrinting:
self.sendCommand("M25")
def setFeedrateModifier(self, type, value): def setFeedrateModifier(self, type, value):
self._feedRateModifier[type] = value self._feedRateModifier[type] = value
@ -563,6 +812,30 @@ class MachineCom(object):
result.update(self._feedRateModifier) result.update(self._feedRateModifier)
return result return result
def enableSdPrinting(self, enable):
if self.isPrinting():
return
self._sdPrinting = enable
def getSdFiles(self):
return self._sdFiles
def startSdFileTransfer(self, filename):
if self.isPrinting() or self.isPaused():
return
self._changeState(self.STATE_RECEIVING_FILE)
self.sendCommand("M28 %s" % filename)
def endSdFileTransfer(self, filename):
self.sendCommand("M29 %s" % filename)
self._changeState(self.STATE_OPERATIONAL)
self.sendCommand("M20")
def deleteSdFile(self, filename):
self.sendCommand("M30 %s" % filename)
self.sendCommand("M20")
def getExceptionString(): def getExceptionString():
locationInfo = traceback.extract_tb(sys.exc_info()[2])[0] 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]) 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])