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._gcodeLoader = None
# sd handling
self._sdPrinting = False
self._sdFile = None
self._sdStreamer = None
# feedrate
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()},
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},
currentZ=None
)
@ -120,7 +126,12 @@ class Printer():
try: callback.sendCurrentData(copy.deepcopy(data))
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):
"""
@ -184,13 +195,19 @@ class Printer():
"""
if self._comm is None or not self._comm.isOperational():
return
if self._gcodeList is None:
if self._gcodeList is None and self._sdFile is None:
return
if self._comm.isPrinting():
return
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):
"""
@ -206,13 +223,17 @@ 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
self._setCurrentZ(None)
self._setProgressData(None, None, None)
self._setProgressData(None, None, None, None)
# mark print as failure
self._gcodeManager.printFailed(self._filename)
@ -250,7 +271,7 @@ class Printer():
self._messages = self._messages[-300:]
self._stateMonitor.addMessage(message)
def _setProgressData(self, progress, printTime, printTimeLeft):
def _setProgressData(self, progress, currentLine, printTime, printTimeLeft):
self._progress = progress
self._printTime = printTime
self._printTimeLeft = printTimeLeft
@ -263,7 +284,7 @@ class Printer():
if (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):
currentTimeUtc = int(time.time() * 1000)
@ -365,13 +386,12 @@ class Printer():
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)
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:
self._gcodeManager.pauseAnalysis() # printing done, put those cpu cycles to good use
self._gcodeManager.pauseAnalysis() # do not analyse gcode while printing
self._setState(state)
def mcMessage(self, message):
"""
Callback method for the comm object, called upon message exchanges via serial.
@ -379,18 +399,22 @@ class Printer():
"""
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.
Triggers storage of new values for printTime, printTimeLeft and the current line.
"""
oldProgress = self._progress
if self._timelapse is not None:
try: self._timelapse.onPrintjobProgress(oldProgress, self._progress, int(round(self._progress * 100 / len(self._gcodeList))))
except: pass
if self._sdPrinting:
newLine = None
(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):
"""
@ -402,6 +426,66 @@ class Printer():
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
def _onGcodeLoadingProgress(self, filename, progress, mode):
@ -414,7 +498,7 @@ class Printer():
def _onGcodeLoaded(self, filename, gcodeList):
self._setJobData(filename, gcodeList)
self._setCurrentZ(None)
self._setProgressData(None, None, None)
self._setProgressData(None, None, None, None)
self._gcodeLoader = None
self._stateMonitor.setGcodeData({"filename": None, "progress": None})
@ -464,10 +548,10 @@ class Printer():
return self._comm is not None and self._comm.isError()
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):
return self._gcodeLoader is not None
return self._gcodeLoader is not None or self._sdStreamer is not None
class GcodeLoader(threading.Thread):
"""
@ -514,6 +598,34 @@ class GcodeLoader(threading.Thread):
def _onParsingProgress(self, progress):
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):
def __init__(self, ratelimit, updateCallback, addTemperatureCallback, addLogCallback, addMessageCallback):
self._ratelimit = ratelimit
@ -525,6 +637,7 @@ class StateMonitor(object):
self._state = None
self._jobData = None
self._gcodeData = None
self._sdUploadData = None
self._currentZ = None
self._progress = None
@ -535,10 +648,11 @@ class StateMonitor(object):
self._worker.daemon = True
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.setJobData(jobData)
self.setGcodeData(gcodeData)
self.setSdUploadData(sdUploadData)
self.setProgress(progress)
self.setCurrentZ(currentZ)
@ -570,6 +684,10 @@ class StateMonitor(object):
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()
@ -594,6 +712,7 @@ class StateMonitor(object):
"state": self._state,
"job": self._jobData,
"gcode": self._gcodeData,
"sdUpload": self._sdUploadData,
"currentZ": self._currentZ,
"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),
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,
enableAccessControl=userManager is not None
enableAccessControl=userManager is not None,
enableSdSupport=settings().get(["feature", "sdSupport"])
)
#~~ Printer control
@ -257,7 +258,19 @@ def getCustomControls():
@app.route(BASEURL + "gcodefiles", methods=["GET"])
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"])
def readGcodeFile(filename):
@ -270,6 +283,8 @@ def uploadGcodeFile():
if "gcode_file" in request.files.keys():
file = request.files["gcode_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)
@app.route(BASEURL + "gcodefiles/load", methods=["POST"])
@ -279,9 +294,14 @@ def loadGcodeFile():
printAfterLoading = False
if "print" in request.values.keys() and request.values["print"] in valid_boolean_trues:
printAfterLoading = True
filename = gcodeManager.getAbsolutePath(request.values["filename"])
if filename is not None:
printer.loadGcode(filename, printAfterLoading)
if "target" in request.values.keys() and request.values["target"] == "sd":
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)
@app.route(BASEURL + "gcodefiles/delete", methods=["POST"])
@ -289,7 +309,10 @@ def loadGcodeFile():
def deleteGcodeFile():
if "filename" in request.values.keys():
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()
#~~ timelapse handling
@ -380,7 +403,8 @@ def getSettings():
},
"feature": {
"gcodeViewer": s.getBoolean(["feature", "gCodeVisualizer"]),
"waitForStart": s.getBoolean(["feature", "waitForStartOnConnect"])
"waitForStart": s.getBoolean(["feature", "waitForStartOnConnect"]),
"sdSupport": s.getBoolean(["feature", "sdSupport"])
},
"folder": {
"uploads": s.getBaseFolder("uploads"),
@ -424,6 +448,7 @@ def setSettings():
if "feature" in data.keys():
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 "sdSupport" in data["feature"].keys(): s.setBoolean(["feature", "sdSupport"], data["feature"]["sdSupport"])
if "folder" in data.keys():
if "uploads" in data["folder"].keys(): s.setBaseFolder("uploads", data["folder"]["uploads"])

View File

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

View File

@ -219,6 +219,7 @@ function PrinterStateViewModel(loginStateViewModel) {
self.estimatedPrintTime = 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.currentHeight = ko.observable(undefined);
@ -229,10 +230,10 @@ function PrinterStateViewModel(loginStateViewModel) {
var currentLine = self.currentLine() ? self.currentLine() : "-";
return currentLine + " / " + self.totalLines();
});
self.progress = ko.computed(function() {
if (!self.currentLine() || !self.totalLines())
self.progressString = ko.computed(function() {
if (!self.progress())
return 0;
return Math.round(self.currentLine() * 100 / self.totalLines());
return self.progress();
});
self.pauseString = ko.computed(function() {
if (self.isPaused())
@ -253,6 +254,7 @@ function PrinterStateViewModel(loginStateViewModel) {
self._processStateData(data.state)
self._processJobData(data.job);
self._processGcodeData(data.gcode);
self._processSdUploadData(data.sdUpload);
self._processProgressData(data.progress);
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.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.printTimeLeft(data.printTimeLeft);
}
@ -754,14 +768,14 @@ function GcodeFilesViewModel(loginStateViewModel) {
return 0;
},
"upload": function(a, b) {
// sorts descending
if (a["date"] > b["date"]) return -1;
if (a["date"] < b["date"]) return 1;
return 0;
// sorts descending
if (b["date"] === undefined || a["date"] > b["date"]) return -1;
if (a["date"] < b["date"]) return 1;
return 0;
},
"size": function(a, b) {
// 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;
return 0;
}
@ -769,10 +783,17 @@ function GcodeFilesViewModel(loginStateViewModel) {
{
"printed": function(file) {
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",
[],
[["sd", "local"]],
CONFIG_GCODEFILESPERPAGE
);
@ -823,20 +844,26 @@ function GcodeFilesViewModel(loginStateViewModel) {
}
self.loadFile = function(filename, printAfterLoad) {
var file = self.listHelper.getItem(function(item) {return item.name == filename});
if (!file) return;
$.ajax({
url: AJAX_BASEURL + "gcodefiles/load",
type: "POST",
dataType: "json",
data: {filename: filename, print: printAfterLoad}
data: {filename: filename, print: printAfterLoad, target: file.origin}
})
}
self.removeFile = function(filename) {
var file = self.listHelper.getItem(function(item) {return item.name == filename});
if (!file) return;
$.ajax({
url: AJAX_BASEURL + "gcodefiles/delete",
type: "POST",
dataType: "json",
data: {filename: filename},
data: {filename: filename, target: file.origin},
success: self.fromResponse
})
}
@ -917,6 +944,7 @@ function TimelapseViewModel(loginStateViewModel) {
},
"name",
[],
[],
CONFIG_TIMELAPSEFILESPERPAGE
)
@ -996,13 +1024,15 @@ function GcodeViewModel(loginStateViewModel) {
self.status = 'idle';
self.enabled = false;
self.errorCount = 0;
self.initialize = function(){
self.enabled = true;
GCODE.ui.initHandlers();
}
self.loadFile = function(filename){
if (self.status == 'idle') {
if (self.status == 'idle' && self.errorCount < 3) {
self.status = 'request';
$.ajax({
url: AJAX_BASEURL + "gcodefiles/" + filename,
@ -1016,6 +1046,7 @@ function GcodeViewModel(loginStateViewModel) {
},
error: function() {
self.status = 'idle';
self.errorCount++;
}
})
}
@ -1045,6 +1076,7 @@ function GcodeViewModel(loginStateViewModel) {
GCODE.renderer.render(cmdIndex.layer, 0, cmdIndex.cmd);
GCODE.ui.updateLayerInfo(cmdIndex.layer);
}
self.errorCount = 0
} else if (data.job.filename) {
self.loadFile(data.job.filename);
}
@ -1198,7 +1230,7 @@ function UsersViewModel(loginStateViewModel) {
if (user === undefined) return;
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"});
return;
}
@ -1267,6 +1299,7 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) {
self.feature_gcodeViewer = ko.observable(undefined);
self.feature_waitForStart = ko.observable(undefined);
self.feature_sdSupport = ko.observable(undefined);
self.folder_uploads = 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_waitForStart(response.feature.waitForStart);
self.feature_sdSupport(response.feature.sdSupport);
self.folder_uploads(response.folder.uploads);
self.folder_timelapse(response.folder.timelapse);
@ -1343,7 +1377,8 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) {
},
"feature": {
"gcodeViewer": self.feature_gcodeViewer(),
"waitForStart": self.feature_waitForStart()
"waitForStart": self.feature_waitForStart(),
"sdSupport": self.feature_sdSupport()
},
"folder": {
"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;
self.listType = listType;
@ -1483,6 +1518,7 @@ function ItemListHelper(listType, supportedSorting, supportedFilters, defaultSor
self.supportedFilters = supportedFilters;
self.defaultSorting = defaultSorting;
self.defaultFilters = defaultFilters;
self.exclusiveFilters = exclusiveFilters;
self.allItems = [];
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
self.changeSorting = function(sorting) {
@ -1604,6 +1651,16 @@ function ItemListHelper(listType, supportedSorting, supportedFilters, defaultSor
if (!_.contains(_.keys(self.supportedFilters), filter))
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();
filters.push(filter);
self.currentFilters(filters);
@ -1613,7 +1670,7 @@ function ItemListHelper(listType, supportedSorting, supportedFilters, defaultSor
}
self.removeFilter = function(filter) {
if (filter != "printed")
if (!_.contains(_.keys(self.supportedFilters), filter))
return;
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_reconnect").click(function() {dataUpdater.reconnect()});
@ -1908,7 +1985,7 @@ $(function() {
} else {
$("#gcode_upload").fileupload("disable");
}
})
});
//~~ UI stuff

View File

@ -24,6 +24,7 @@
var CONFIG_USERSPERPAGE = 10;
var CONFIG_WEBCAM_STREAM = "{{ webcamStream }}";
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_DEBUG = true;
@ -116,7 +117,7 @@
Print Time Left: <strong data-bind="text: printTimeLeft"></strong><br>
<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 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('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>
{% 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><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>
@ -181,6 +187,11 @@
<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()">
</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 class="bar" style="width: 0%"></div>
</div>

View File

@ -107,6 +107,13 @@
</label>
</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>
</div>
<div class="tab-pane" id="settings_folder">

View File

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

View File

@ -64,9 +64,24 @@ class VirtualPrinter():
self.bedTemp = 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):
if self.readList is None:
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())
if 'M104' in data or 'M109' in data:
try:
@ -78,11 +93,121 @@ class VirtualPrinter():
self.bedTargetTemp = float(re.search('S([0-9]+)', data).group(1))
except:
pass
if 'M105' in data:
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:
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):
if self.readList is None:
return ''
@ -105,7 +230,6 @@ class VirtualPrinter():
if self.readList is None:
return ''
time.sleep(0.001)
#print "Recv: %s" % (self.readList[0].rstrip())
return self.readList.pop(0)
def close(self):
@ -124,12 +248,21 @@ class MachineComPrintCallback(object):
def mcMessage(self, message):
pass
def mcProgress(self, lineNr):
def mcProgress(self):
pass
def mcZChange(self, newZ):
pass
def mcSdFiles(self, files):
pass
def mcSdSelected(self, filename, size):
pass
def mcSdPrintingDone(self):
pass
class MachineCom(object):
STATE_NONE = 0
STATE_OPEN_SERIAL = 1
@ -142,6 +275,7 @@ class MachineCom(object):
STATE_CLOSED = 8
STATE_ERROR = 9
STATE_CLOSED_WITH_ERROR = 10
STATE_RECEIVING_FILE = 11
def __init__(self, port = None, baudrate = None, callbackObject = None):
self._logger = logging.getLogger(__name__)
@ -177,12 +311,19 @@ class MachineCom(object):
self._currentZ = -1
self._heatupWaitStartTime = 0
self._heatupWaitTimeLost = 0.0
self._printStartTime100 = None
self._printStartTime = None
self.thread = threading.Thread(target=self._monitor)
self.thread.daemon = True
self.thread.start()
self._sdPrinting = False
self._sdFileList = False
self._sdFile = None
self._sdFilePos = None
self._sdFileSize = None
self._sdFiles = []
def _changeState(self, newState):
if self._state == newState:
return
@ -208,7 +349,10 @@ class MachineCom(object):
if self._state == self.STATE_OPERATIONAL:
return "Operational"
if self._state == self.STATE_PRINTING:
return "Printing"
if self._sdPrinting:
return "Printing from SD"
else:
return "Printing"
if self._state == self.STATE_PAUSED:
return "Paused"
if self._state == self.STATE_CLOSED:
@ -217,6 +361,8 @@ class MachineCom(object):
return "Error: %s" % (self.getShortErrorString())
if self._state == self.STATE_CLOSED_WITH_ERROR:
return "Error: %s" % (self.getShortErrorString())
if self._state == self.STATE_RECEIVING_FILE:
return "Streaming file to SD"
return "?%d?" % (self._state)
def getShortErrorString(self):
@ -234,31 +380,57 @@ class MachineCom(object):
return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR
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):
return self._state == self.STATE_PRINTING
def isSdPrinting(self):
return self._sdPrinting
def isPaused(self):
return self._state == self.STATE_PAUSED
def isBusy(self):
return self.isPrinting() or self._state == self.STATE_RECEIVING_FILE
def getPrintPos(self):
return self._gcodePos
if self._sdPrinting:
return self._sdFilePos
else:
return self._gcodePos
def getPrintTime(self):
if self._printStartTime100 == None:
if self._printStartTime == None:
return 0
else:
return time.time() - self._printStartTime100
return time.time() - self._printStartTime
def getPrintTimeRemainingEstimate(self):
if self._printStartTime100 == None or self.getPrintPos() < 200:
if self._printStartTime == None:
return None
printTime = (time.time() - self._printStartTime100) / 60
printTimeTotal = printTime * (len(self._gcodeList) - 100) / (self.getPrintPos() - 100)
printTimeLeft = printTimeTotal - printTime
return printTimeLeft
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
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):
return self._temp
@ -318,13 +490,15 @@ class MachineCom(object):
#Start monitoring the serial port.
timeout = time.time() + 5
tempRequestTimeout = timeout
sdStatusRequestTimeout = timeout
startSeen = not settings().getBoolean(["feature", "waitForStartOnConnect"])
while True:
line = self._readline()
if line == None:
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:'):
#Oh YEAH, consistency.
# 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():
self._errorValue = line[6:]
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:'):
self._temp = float(re.search("-?[0-9\.]*", line.split('T:')[1]).group(0))
if ' B:' in line:
@ -348,6 +530,36 @@ class MachineCom(object):
t = time.time()
self._heatupWaitTimeLost = t - self._heatupWaitStartTime
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():
self._callback.mcMessage(line)
@ -395,6 +607,8 @@ class MachineCom(object):
startSeen = True
elif 'ok' in line and startSeen:
self._changeState(self.STATE_OPERATIONAL)
if settings().get(["feature", "sdSupport"]):
self._sendCommand("M20")
elif time.time() > timeout:
self.close()
elif self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PAUSED:
@ -403,25 +617,35 @@ class MachineCom(object):
self._sendCommand("M105")
tempRequestTimeout = time.time() + 5
elif self._state == self.STATE_PRINTING:
if line == '' and time.time() > timeout:
self._log("Communication timeout during printing, forcing a line")
line = 'ok'
#Even when printing request the temperture every 5 seconds.
# Even when printing request the temperture every 5 seconds.
if time.time() > tempRequestTimeout:
self._commandQueue.put("M105")
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():
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])
else:
if line == '' and time.time() > timeout:
self._log("Communication timeout during printing, forcing a line")
line = 'ok'
if 'ok' in line:
timeout = time.time() + 5
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")
def _log(self, message):
@ -501,7 +725,7 @@ class MachineCom(object):
self._changeState(self.STATE_OPERATIONAL)
return
if self._gcodePos == 100:
self._printStartTime100 = time.time()
self._printStartTime = time.time()
line = self._gcodeList[self._gcodePos]
if type(line) is tuple:
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)))
self._sendCommand("N%d%s*%d" % (self._gcodePos, line, checksum))
self._gcodePos += 1
self._callback.mcProgress(self._gcodePos)
self._callback.mcProgress()
def sendCommand(self, cmd):
cmd = cmd.encode('ascii', 'replace')
@ -536,25 +760,50 @@ class MachineCom(object):
return
self._gcodeList = gcodeList
self._gcodePos = 0
self._printStartTime100 = None
self._printSection = 'CUSTOM'
self._changeState(self.STATE_PRINTING)
self._printStartTime = time.time()
for i in xrange(0, 6):
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):
if self.isOperational():
self._changeState(self.STATE_OPERATIONAL)
if self._sdPrinting:
self.sendCommand("M25")
def setPause(self, pause):
if not pause and self.isPaused():
self._changeState(self.STATE_PRINTING)
for i in xrange(0, 6):
self._sendNext()
if self._sdPrinting:
self.sendCommand("M24")
else:
for i in xrange(0, 6):
self._sendNext()
if pause and self.isPrinting():
self._changeState(self.STATE_PAUSED)
if self._sdPrinting:
self.sendCommand("M25")
def setFeedrateModifier(self, type, value):
self._feedRateModifier[type] = value
@ -563,6 +812,30 @@ class MachineCom(object):
result.update(self._feedRateModifier)
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():
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])