Merge branch 'devel' into events

Conflicts:
	octoprint/printer.py
	octoprint/server.py
	octoprint/util/comm.py
master
Gina Häußge 2013-06-16 11:50:52 +02:00
commit ed9e93f379
13 changed files with 742 additions and 503 deletions

View File

@ -6,17 +6,17 @@ OctoPrint
OctoPrint provides a responsive web interface for controlling a 3D printer (RepRap, Ultimaker, ...). It currently
allows
* uploading .gcode files to the server and managing them via the UI
* selecting a file for printing, getting the usual stats regarding filament length etc (stats can be disabled for
faster initial processing)
* uploading .gcode files to the server plus optionally the printer's SD card and managing them via the UI
* selecting a file for printing, getting the usual stats regarding filament length etc (stats not available for SD files)
* starting, pausing and canceling a print job
* while connected to the printer, gaining information regarding the current temperature of both head and bed (if available) in a nice shiny javascript-y temperature graph
* while printing, gaining information regarding the current progress of the print job (height, percentage etc)
* reading the communication log and send arbitrary codes to be executed by the printer
* moving the X, Y and Z axis (jog controls), extruding, retracting and custom controls
* optional: previewing the GCODE of the selected model to print (via gCodeVisualizer), including rendering of the progress during printing
* previewing the GCODE of the selected model to print (via gCodeVisualizer), including rendering of the progress during printing (not available when SD printing)
* optional: visual monitoring of the printer via webcam stream integrated into the UI (using e.g. MJPG-Streamer)
* optional: creation of timelapse recordings of the printjob via webcam stream (using e.g. MJPG-Streamer) -- currently two timelaspe methods are implemented, triggering a shot on z-layer change or every "n" seconds
* optional: access control to provide a read-only mode on the web interface, allowing any actions only to logged in users
The intended usecase is to run OctoPrint on a single-board computer like the Raspberry Pi and a WiFi module,
connect the printer to the server and therefore create a WiFi-enabled 3D printer. If you want to add a webcam for visual
@ -54,7 +54,7 @@ Alternatively, the host and port on which to bind can be defined via the configu
If you want to run OctoPrint as a daemon (only supported on Linux), use
./run --daemon {start|stop|restart} [--pid PIDFILE]
./run --daemon {start|stop|restart} [--pid PIDFILE]
If you do not supply a custom pidfile location via `--pid PIDFILE`, it will be created at `/tmp/octoprint.pid`.
@ -106,8 +106,3 @@ The following software is recommended for Webcam support on the Raspberry Pi:
* MJPG-Streamer: http://sourceforge.net/apps/mediawiki/mjpg-streamer/index.php?title=Main_Page
I also want to thank [Janina Himmen](http://jhimmen.de/) for providing the kick-ass logo!
Why is it called OctoPrint and what's with the crystal ball in the logo?
------------------------------------------------------------------------
It so happens that I needed a favicon and also OctoPrint's first name -- Printer WebUI -- simply lacked a certain coolness to it. So I asked The Internet(tm) for advise. After some brainstorming, the idea of a cute Octopus watching his print job remotely through a crystal ball was born... [or something like that](https://plus.google.com/u/0/106003970953341660077/posts/UmLD5mW8yBQ).

View File

@ -61,18 +61,17 @@ class Printer():
self._printTime = None
self._printTimeLeft = None
# gcode handling
self._gcodeList = None
self._filename = None
self._gcodeLoader = None
self._printAfterSelect = False
# sd handling
self._sdPrinting = False
self._sdStreaming = False
# TODO Still needed?
self._sdFile = None
self._sdStreamer = None
# feedrate
self._feedrateModifierMapping = {"outerWall": "WALL-OUTER", "innerWall": "WALL_INNER", "fill": "FILL", "support": "SUPPORT"}
self._selectedFile = None
# timelapse
self._timelapse = None
@ -93,10 +92,8 @@ class Printer():
)
self._stateMonitor.reset(
state={"state": None, "stateString": self.getStateString(), "flags": self._getStateFlags()},
jobData={"filename": None, "lines": None, "estimatedPrintTime": None, "filament": None},
gcodeData={"filename": None, "progress": None},
sdUploadData={"filename": None, "progress": None},
progress={"progress": None, "printTime": None, "printTimeLeft": None},
jobData={"filename": None, "filesize": None, "estimatedPrintTime": None, "filament": None},
progress={"progress": None, "filepos": None, "printTime": None, "printTimeLeft": None},
currentZ=None
)
@ -168,52 +165,25 @@ class Printer():
for command in commands:
self._comm.sendCommand(command)
def setFeedrateModifier(self, structure, percentage):
if (not self._feedrateModifierMapping.has_key(structure)) or percentage < 0:
def selectFile(self, filename, sd, printAfterSelect=False):
if self._comm is not None and (self._comm.isBusy() or self._comm.isStreaming()):
return
self._comm.setFeedrateModifier(self._feedrateModifierMapping[structure], percentage / 100.0)
self._printAfterSelect = printAfterSelect
self._comm.selectFile(filename, sd)
def loadGcode(self, file, printAfterLoading=False):
"""
Loads the gcode from the given file as the new print job.
Aborts if the printer is currently printing or another gcode file is currently being loaded.
"""
if (self._comm is not None and self._comm.isPrinting()) or (self._gcodeLoader is not None):
return
self._sdFile = None
self._setJobData(None, None)
onGcodeLoadedCallback = self._onGcodeLoaded
if printAfterLoading:
onGcodeLoadedCallback = self._onGcodeLoadedToPrint
self._gcodeLoader = GcodeLoader(file, self._onGcodeLoadingProgress, onGcodeLoadedCallback)
self._gcodeLoader.start()
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
def startPrint(self):
"""
Starts the currently loaded print job.
Only starts if the printer is connected and operational, not currently printing and a printjob is loaded
"""
if self._comm is None or not self._comm.isOperational():
if self._comm is None or not self._comm.isOperational() or self._comm.isPrinting():
return
if self._gcodeList is None and self._sdFile is None:
return
if self._comm.isPrinting():
if self._selectedFile is None:
return
self._setCurrentZ(-1)
if self._sdFile is not None:
# we are working in sd mode
self._sdPrinting = True
self._comm.printSdFile()
else:
# we are working in local mode
self._comm.printGCode(self._gcodeList)
self._setCurrentZ(None)
self._comm.startPrint()
def togglePausePrint(self):
"""
@ -221,6 +191,7 @@ class Printer():
"""
if self._comm is None:
return
self._comm.setPause(not self._comm.isPaused())
def cancelPrint(self, disableMotorsAndHeater=True):
@ -230,23 +201,19 @@ class Printer():
if self._comm is None:
return
if self._sdPrinting:
self._sdPrinting = False
self._comm.cancelPrint()
if disableMotorsAndHeater:
self.commands(["M84", "M104 S0", "M140 S0", "M106 S0"]) # disable motors, switch off heaters and fan
# reset line, height, print time
# reset progress, height, print time
self._setCurrentZ(None)
self._setProgressData(None, None, None, None)
# mark print as failure
if self._filename is not None:
self._gcodeManager.printFailed(self._filename)
if self._selectedFile is not None:
self._gcodeManager.printFailed(self._selectedFile["filename"])
eventManager().fire("PrintFailed", self._filename)
#~~ state monitoring
def setTimelapse(self, timelapse):
if self._timelapse is not None and self.isPrinting():
@ -257,6 +224,8 @@ class Printer():
def getTimelapse(self):
return self._timelapse
#~~ state monitoring
def _setCurrentZ(self, currentZ):
self._currentZ = currentZ
@ -279,7 +248,7 @@ class Printer():
self._messages = self._messages[-300:]
self._stateMonitor.addMessage(message)
def _setProgressData(self, progress, currentLine, printTime, printTimeLeft):
def _setProgressData(self, progress, filepos, printTime, printTimeLeft):
self._progress = progress
self._printTime = printTime
self._printTimeLeft = printTimeLeft
@ -292,7 +261,11 @@ class Printer():
if (self._printTimeLeft):
formattedPrintTimeLeft = util.getFormattedTimeDelta(datetime.timedelta(minutes=self._printTimeLeft))
self._stateMonitor.setProgress({"progress": self._progress, "currentLine": currentLine, "printTime": formattedPrintTime, "printTimeLeft": formattedPrintTimeLeft})
formattedFilePos = None
if (filepos):
formattedFilePos = util.getFormattedSize(filepos)
self._stateMonitor.setProgress({"progress": self._progress, "filepos": formattedFilePos, "printTime": formattedPrintTime, "printTimeLeft": formattedPrintTimeLeft})
def _addTemperatureData(self, temp, bedTemp, targetTemp, bedTargetTemp):
currentTimeUtc = int(time.time() * 1000)
@ -316,19 +289,25 @@ class Printer():
self._stateMonitor.addTemperature({"currentTime": currentTimeUtc, "temp": self._temp, "bedTemp": self._bedTemp, "targetTemp": self._targetTemp, "targetBedTemp": self._targetBedTemp})
def _setJobData(self, filename, gcodeList):
self._filename = filename
self._gcodeList = gcodeList
lines = None
if self._gcodeList:
lines = len(self._gcodeList)
def _setJobData(self, filename, filesize, sd):
if filename is not None:
self._selectedFile = {
"filename": filename,
"filesize": filesize,
"sd": sd
}
else:
self._selectedFile = None
formattedFilename = None
formattedFilesize = None
estimatedPrintTime = None
filament = None
if self._filename:
formattedFilename = os.path.basename(self._filename)
if filename:
formattedFilename = os.path.basename(filename)
if filesize:
formattedFilesize = util.getFormattedSize(filesize)
fileData = self._gcodeManager.getFileData(filename)
if fileData is not None and "gcodeAnalysis" in fileData.keys():
@ -337,7 +316,7 @@ class Printer():
if "filament" in fileData["gcodeAnalysis"].keys():
filament = fileData["gcodeAnalysis"]["filament"]
self._stateMonitor.setJobData({"filename": formattedFilename, "lines": lines, "estimatedPrintTime": estimatedPrintTime, "filament": filament})
self._stateMonitor.setJobData({"filename": formattedFilename, "filesize": formattedFilesize, "estimatedPrintTime": estimatedPrintTime, "filament": filament, "sd": sd})
def _sendInitialStateUpdate(self, callback):
try:
@ -364,7 +343,6 @@ class Printer():
"printing": self.isPrinting(),
"closedOrError": self.isClosedOrError(),
"error": self.isError(),
"loading": self.isLoading(),
"paused": self.isPaused(),
"ready": self.isReady(),
"sdReady": sdReady
@ -395,14 +373,15 @@ class Printer():
if oldState == self._comm.STATE_PRINTING and state != self._comm.STATE_PAUSED:
self._timelapse.onPrintjobStopped()
elif state == self._comm.STATE_PRINTING and oldState != self._comm.STATE_PAUSED:
self._timelapse.onPrintjobStarted(self._filename)
self._timelapse.onPrintjobStarted(self._selectedFile["filename"])
# forward relevant state changes to gcode manager
if self._comm is not None and oldState == self._comm.STATE_PRINTING:
if state == self._comm.STATE_OPERATIONAL:
self._gcodeManager.printSucceeded(self._filename)
elif state == self._comm.STATE_CLOSED or state == self._comm.STATE_ERROR or state == self._comm.STATE_CLOSED_WITH_ERROR:
self._gcodeManager.printFailed(self._filename)
if self._selectedFile is not None:
if state == self._comm.STATE_OPERATIONAL:
self._gcodeManager.printSucceeded(self._selectedFile["filename"])
elif state == self._comm.STATE_CLOSED or state == self._comm.STATE_ERROR or state == self._comm.STATE_CLOSED_WITH_ERROR:
self._gcodeManager.printFailed(self._selectedFile["filename"])
self._gcodeManager.resumeAnalysis() # printing done, put those cpu cycles to good use
elif self._comm is not None and state == self._comm.STATE_PRINTING:
self._gcodeManager.pauseAnalysis() # do not analyse gcode while printing
@ -419,25 +398,10 @@ class Printer():
def mcProgress(self):
"""
Callback method for the comm object, called upon any change in progress of the printjob.
Triggers storage of new values for printTime, printTimeLeft and the current line.
Triggers storage of new values for printTime, printTimeLeft and the current progress.
"""
oldProgress = self._progress
if self._sdPrinting:
newLine = None
(filePos, fileSize) = self._comm.getSdProgress()
if fileSize > 0:
newProgress = float(filePos) / float(fileSize)
else:
newProgress = 0.0
else:
newLine = self._comm.getPrintPos()
if self._gcodeList is not None:
newProgress = float(newLine) / float(len(self._gcodeList))
else:
newProgress = 0.0
self._setProgressData(newProgress, newLine, self._comm.getPrintTime(), self._comm.getPrintTimeRemainingEstimate())
self._setProgressData(self._comm.getPrintProgress(), self._comm.getPrintFilepos(), self._comm.getPrintTime(), self._comm.getPrintTimeRemainingEstimate())
def mcZChange(self, newZ):
"""
@ -460,33 +424,49 @@ class Printer():
def mcSdFiles(self, files):
self._sendTriggerUpdateCallbacks("gcodeFiles")
def mcSdSelected(self, filename, filesize):
self._sdFile = filename
self._setJobData(filename, None)
def mcFileSelected(self, filename, filesize, sd):
self._setJobData(filename, filesize, sd)
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
if self._sdPrintAfterSelect:
if self._printAfterSelect:
self.startPrint()
def mcSdPrintingDone(self):
self._sdPrinting = False
self._setProgressData(1.0, None, self._comm.getPrintTime(), self._comm.getPrintTimeRemainingEstimate())
def mcPrintjobDone(self):
self._setProgressData(1.0, self._selectedFile["filesize"], self._comm.getPrintTime(), 0)
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
#~~ sd file handling
def mcFileTransferStarted(self, filename, filesize):
self._sdStreaming = True
self._selectedFile = {
"filename": filename,
"filesize": filesize,
"sd": True
}
self._setJobData(filename, filesize, True)
self._setProgressData(0.0, 0, 0, None)
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
def mcFileTransferDone(self):
self._sdStreaming = False
self._selectedFile = None
self._setCurrentZ(None)
self._setJobData(None, None, None)
self._setProgressData(None, None, None, None)
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
#~~ sd file handling
def getSdFiles(self):
if self._comm is None:
return
return self._comm.getSdFiles()
def addSdFile(self, filename, file):
if not self._comm:
def addSdFile(self, filename, path):
if not self._comm or self._comm.isBusy():
return
self._sdStreamer = SdFileStreamer(self._comm, filename, file, self._onSdFileStreamProgress, self._onSdFileStreamFinish)
self._sdStreamer.start()
self._comm.startFileTransfer(path, filename[:8].lower() + ".gco")
def deleteSdFile(self, filename):
if not self._comm:
@ -496,13 +476,6 @@ class Printer():
self._sdFile = None
self._comm.deleteSdFile(filename)
def selectSdFile(self, filename, printAfterSelect):
if not self._comm:
return
self._sdPrintAfterSelect = printAfterSelect
self._comm.selectSdFile(filename)
def initSdCard(self):
if not self._comm:
return
@ -518,57 +491,8 @@ class Printer():
return
self._comm.refreshSdFiles()
#~~ callbacks triggered by sdFileStreamer
def _onSdFileStreamProgress(self, filename, progress):
self._stateMonitor.setSdUploadData({"filename": filename, "progress": progress})
def _onSdFileStreamFinish(self, filename):
self._setCurrentZ(None)
self._setProgressData(None, None, None, None)
self._sdStreamer = None
self._stateMonitor.setSdUploadData({"filename": None, "progress": None})
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
#~~ callbacks triggered by gcodeLoader
def _onGcodeLoadingProgress(self, filename, progress, mode):
formattedFilename = None
if filename is not None:
formattedFilename = os.path.basename(filename)
self._stateMonitor.setGcodeData({"filename": formattedFilename, "progress": progress, "mode": mode})
def _onGcodeLoaded(self, filename, gcodeList):
self._setJobData(filename, gcodeList)
self._setCurrentZ(None)
self._setProgressData(None, None, None, None)
self._gcodeLoader = None
self._stateMonitor.setGcodeData({"filename": None, "progress": None})
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
eventManager().fire("LoadDone", filename)
def _onGcodeLoadedToPrint(self, filename, gcodeList):
self._onGcodeLoaded(filename, gcodeList)
self.startPrint()
#~~ state reports
def feedrateState(self):
if self._comm is not None:
feedrateModifiers = self._comm.getFeedrateModifiers()
result = {}
for structure in self._feedrateModifierMapping.keys():
if (feedrateModifiers.has_key(self._feedrateModifierMapping[structure])):
result[structure] = int(round(feedrateModifiers[self._feedrateModifierMapping[structure]] * 100))
else:
result[structure] = 100
return result
else:
return None
def getStateString(self):
"""
Returns a human readable string corresponding to the current communication state.
@ -578,6 +502,21 @@ class Printer():
else:
return self._comm.getStateString()
def getCurrentData(self):
return self._stateMonitor.getCurrentData()
def getCurrentTemperatures(self):
return {
"extruder": {
"current": self._temp,
"target": self._targetTemp
},
"bed": {
"current": self._bedTemp,
"target": self._targetBedTemp
}
}
def isClosedOrError(self):
return self._comm is None or self._comm.isClosedOrError()
@ -594,13 +533,11 @@ class Printer():
return self._comm is not None and self._comm.isError()
def isReady(self):
return self._gcodeLoader is None and self._sdStreamer is None and ((self._gcodeList and len(self._gcodeList) > 0) or self._sdFile)
return self.isOperational() and not self._comm.isStreaming()
def isLoading(self):
return self._gcodeLoader is not None or self._sdStreamer is not None
class GcodeLoader(threading.Thread):
"""
The GcodeLoader takes care of loading a gcode-File from disk and parsing it into a gcode object in a separate
@ -661,7 +598,7 @@ class SdFileStreamer(threading.Thread):
return
name = self._filename[:self._filename.rfind(".")]
sdFilename = name[:8] + ".GCO"
sdFilename = name[:8].lower() + ".gco"
try:
size = os.stat(self._file).st_size
with open(self._file, "r") as f:
@ -701,11 +638,9 @@ class StateMonitor(object):
self._worker.daemon = True
self._worker.start()
def reset(self, state=None, jobData=None, gcodeData=None, sdUploadData=None, progress=None, currentZ=None):
def reset(self, state=None, jobData=None, progress=None, currentZ=None):
self.setState(state)
self.setJobData(jobData)
self.setGcodeData(gcodeData)
self.setSdUploadData(sdUploadData)
self.setProgress(progress)
self.setCurrentZ(currentZ)
@ -733,14 +668,6 @@ class StateMonitor(object):
self._jobData = jobData
self._changeEvent.set()
def setGcodeData(self, gcodeData):
self._gcodeData = gcodeData
self._changeEvent.set()
def setSdUploadData(self, uploadData):
self._sdUploadData = uploadData
self._changeEvent.set()
def setProgress(self, progress):
self._progress = progress
self._changeEvent.set()
@ -764,8 +691,6 @@ class StateMonitor(object):
return {
"state": self._state,
"job": self._jobData,
"gcode": self._gcodeData,
"sdUpload": self._sdUploadData,
"currentZ": self._currentZ,
"progress": self._progress
}

View File

@ -240,23 +240,6 @@ def jog():
return jsonify(SUCCESS)
@app.route(BASEURL + "control/speed", methods=["GET"])
def getSpeedValues():
return jsonify(feedrate=printer.feedrateState())
@app.route(BASEURL + "control/speed", methods=["POST"])
@login_required
def speed():
if not printer.isOperational():
return jsonify(SUCCESS)
for key in ["outerWall", "innerWall", "fill", "support"]:
if key in request.values.keys():
value = int(request.values[key])
printer.setFeedrateModifier(key, value)
return getSpeedValues()
@app.route(BASEURL + "control/custom", methods=["GET"])
def getCustomControls():
customControls = settings().get(["controls"])
@ -279,6 +262,17 @@ def sdCommand():
return jsonify(SUCCESS)
#~~ Printer State
@app.route(BASEURL + "state", methods=["GET"])
@login_required
def getPrinterState():
currentData = printer.getCurrentData()
currentData.update({
"temperatures": printer.getCurrentTemperatures()
})
return jsonify(currentData)
#~~ GCODE file handling
@app.route(BASEURL + "gcodefiles", methods=["GET"])
@ -323,16 +317,14 @@ def loadGcodeFile():
if "print" in request.values.keys() and request.values["print"] in valid_boolean_trues:
printAfterLoading = True
sd = False
filename = None
if "target" in request.values.keys() and request.values["target"] == "sd":
filename = request.values["filename"]
printer.selectSdFile(filename, printAfterLoading)
sd = True
else:
filename = gcodeManager.getAbsolutePath(request.values["filename"])
if filename is not None:
printer.loadGcode(filename, printAfterLoading)
global eventManager
eventManager.fire("LoadStart", filename)
printer.selectFile(filename, sd, printAfterLoading)
return jsonify(SUCCESS)
@app.route(BASEURL + "gcodefiles/delete", methods=["POST"])

View File

@ -43,7 +43,7 @@ default_settings = {
"waitForWaitOnConnect": False,
"alwaysSendChecksum": False,
"resetLineNumbersWithPrefixedN": False,
"sdSupport": False
"sdSupport": True
},
"folder": {
"uploads": None,

View File

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

View File

@ -223,42 +223,31 @@
var assumeNonDC = false;
for(var i=0;i<gcode.length;i++){
// for(var len = gcode.length- 1, i=0;i!=len;i++){
x=undefined;
y=undefined;
z=undefined;
retract = 0;
var line = gcode[i].line;
var percentage = gcode[i].percentage;
extrude=false;
gcode[i] = gcode[i].split(/[\(;]/)[0];
line = line.split(/[\(;]/)[0];
// prevRetract=0;
// retract=0;
// if(gcode[i].match(/^(?:G0|G1)\s+/i)){
if(reg.test(gcode[i])){
var args = gcode[i].split(/\s/);
if(reg.test(line)){
var args = line.split(/\s/);
for(j=0;j<args.length;j++){
// console.log(args);
// if(!args[j])continue;
switch(argChar = args[j].charAt(0).toLowerCase()){
case 'x':
x=args[j].slice(1);
// if(x === prevX){
// x=undefined;
// }
break;
case 'y':
y=args[j].slice(1);
// if(y===prevY){
// y=undefined;
// }
break;
case 'z':
z=args[j].slice(1);
z = Number(z);
if(z == prevZ)continue;
// z = Number(z);
if(z == prevZ) continue;
if(z_heights.hasOwnProperty(z)){
layer = z_heights[z];
}else{
@ -267,9 +256,6 @@
}
sendLayer = layer;
sendLayerZ = z;
// if(parseFloat(prevZ) < )
// if(args[j].charAt(1) === "-")layer--;
// else layer++;
prevZ = z;
break;
case 'e':
@ -292,13 +278,11 @@
retract = -1;
}
else if(prev_extrude["abs"]==0){
// if(prevRetract <0 )prevRetract=retract;
retract = 0;
}else if(prev_extrude["abs"]>0&&prevRetract < 0){
prevRetract = 0;
retract = 1;
} else {
// prevRetract = retract;
retract = 0;
}
prev_extrude[argChar] = numSlice;
@ -317,24 +301,24 @@
prev_extrude["abs"] = Math.sqrt((prevX-x)*(prevX-x)+(prevY-y)*(prevY-y));
}
if(!model[layer])model[layer]=[];
if(typeof(x) !== 'undefined' || typeof(y) !== 'undefined' ||typeof(z) !== 'undefined'||retract!=0) model[layer][model[layer].length] = {x: Number(x), y: Number(y), z: Number(z), extrude: extrude, retract: Number(retract), noMove: false, extrusion: (extrude||retract)?Number(prev_extrude["abs"]):0, prevX: Number(prevX), prevY: Number(prevY), prevZ: Number(prevZ), speed: Number(lastF), gcodeLine: Number(i)};
if(typeof(x) !== 'undefined' || typeof(y) !== 'undefined' ||typeof(z) !== 'undefined'||retract!=0) model[layer][model[layer].length] = {x: Number(x), y: Number(y), z: Number(z), extrude: extrude, retract: Number(retract), noMove: false, extrusion: (extrude||retract)?Number(prev_extrude["abs"]):0, prevX: Number(prevX), prevY: Number(prevY), prevZ: Number(prevZ), speed: Number(lastF), gcodeLine: Number(i), percentage: percentage};
//{x: x, y: y, z: z, extrude: extrude, retract: retract, noMove: false, extrusion: (extrude||retract)?prev_extrude["abs"]:0, prevX: prevX, prevY: prevY, prevZ: prevZ, speed: lastF, gcodeLine: i};
if(typeof(x) !== 'undefined') prevX = x;
if(typeof(y) !== 'undefined') prevY = y;
} else if(gcode[i].match(/^(?:M82)/i)){
} else if(line.match(/^(?:M82)/i)){
extrudeRelative = false;
}else if(gcode[i].match(/^(?:G91)/i)){
}else if(line.match(/^(?:G91)/i)){
extrudeRelative=true;
}else if(gcode[i].match(/^(?:G90)/i)){
}else if(line.match(/^(?:G90)/i)){
extrudeRelative=false;
}else if(gcode[i].match(/^(?:M83)/i)){
}else if(line.match(/^(?:M83)/i)){
extrudeRelative=true;
}else if(gcode[i].match(/^(?:M101)/i)){
}else if(line.match(/^(?:M101)/i)){
dcExtrude=true;
}else if(gcode[i].match(/^(?:M103)/i)){
}else if(line.match(/^(?:M103)/i)){
dcExtrude=false;
}else if(gcode[i].match(/^(?:G92)/i)){
var args = gcode[i].split(/\s/);
}else if(line.match(/^(?:G92)/i)){
var args = line.split(/\s/);
for(j=0;j<args.length;j++){
switch(argChar = args[j].charAt(0).toLowerCase()){
case 'x':
@ -354,16 +338,15 @@
else {
prev_extrude[argChar] = numSlice;
}
// prevZ = z;
break;
default:
break;
}
}
if(!model[layer])model[layer]=[];
if(typeof(x) !== 'undefined' || typeof(y) !== 'undefined' ||typeof(z) !== 'undefined') model[layer][model[layer].length] = {x: parseFloat(x), y: parseFloat(y), z: parseFloat(z), extrude: extrude, retract: parseFloat(retract), noMove: true, extrusion: (extrude||retract)?parseFloat(prev_extrude["abs"]):0, prevX: parseFloat(prevX), prevY: parseFloat(prevY), prevZ: parseFloat(prevZ), speed: parseFloat(lastF),gcodeLine: parseFloat(i)};
}else if(gcode[i].match(/^(?:G28)/i)){
var args = gcode[i].split(/\s/);
if(typeof(x) !== 'undefined' || typeof(y) !== 'undefined' ||typeof(z) !== 'undefined') model[layer][model[layer].length] = {x: parseFloat(x), y: parseFloat(y), z: parseFloat(z), extrude: extrude, retract: parseFloat(retract), noMove: true, extrusion: (extrude||retract)?parseFloat(prev_extrude["abs"]):0, prevX: parseFloat(prevX), prevY: parseFloat(prevY), prevZ: parseFloat(prevZ), speed: parseFloat(lastF),gcodeLine: parseFloat(i), percentage: percentage};
}else if(line.match(/^(?:G28)/i)){
var args = line.split(/\s/);
for(j=0;j<args.length;j++){
switch(argChar = args[j].charAt(0).toLowerCase()){
case 'x':
@ -405,17 +388,11 @@
}
prevZ = z;
}
// x=0, y=0,z=0,prevZ=0, extrude=false;
// if(typeof(prevX) === 'undefined'){prevX=0;}
// if(typeof(prevY) === 'undefined'){prevY=0;}
if(!model[layer])model[layer]=[];
if(typeof(x) !== 'undefined' || typeof(y) !== 'undefined' ||typeof(z) !== 'undefined'||retract!=0) model[layer][model[layer].length] = {x: Number(x), y: Number(y), z: Number(z), extrude: extrude, retract: Number(retract), noMove: false, extrusion: (extrude||retract)?Number(prev_extrude["abs"]):0, prevX: Number(prevX), prevY: Number(prevY), prevZ: Number(prevZ), speed: Number(lastF), gcodeLine: Number(i)};
// if(typeof(x) !== 'undefined' || typeof(y) !== 'undefined' ||typeof(z) !== 'undefined') model[layer][model[layer].length] = {x: x, y: y, z: z, extrude: extrude, retract: retract, noMove:false, extrusion: (extrude||retract)?prev_extrude["abs"]:0, prevX: prevX, prevY: prevY, prevZ: prevZ, speed: lastF, gcodeLine: parseFloat(i)};
if(typeof(x) !== 'undefined' || typeof(y) !== 'undefined' ||typeof(z) !== 'undefined'||retract!=0) model[layer][model[layer].length] = {x: Number(x), y: Number(y), z: Number(z), extrude: extrude, retract: Number(retract), noMove: false, extrusion: (extrude||retract)?Number(prev_extrude["abs"]):0, prevX: Number(prevX), prevY: Number(prevY), prevZ: Number(prevZ), speed: Number(lastF), gcodeLine: Number(i), percentage: percentage};
}
if(typeof(sendLayer) !== "undefined"){
// sendLayerToParent(sendLayer, sendLayerZ, i/gcode.length*100);
// sendLayer = undefined;
if(i-lastSend > gcode.length*0.02 && sendMultiLayer.length != 0){
lastSend = i;
@ -429,13 +406,7 @@
sendLayerZ = undefined;
}
}
// sendMultiLayer[sendMultiLayer.length] = layer;
// sendMultiLayerZ[sendMultiLayerZ.length] = z;
sendMultiLayerToParent(sendMultiLayer, sendMultiLayerZ, i/gcode.length*100);
// if(gCodeOptions["sortLayers"])sortLayers();
// if(gCodeOptions["purgeEmptyLayers"])purgeLayers();
};

View File

@ -23,57 +23,73 @@ GCODE.gCodeReader = (function(){
purgeEmptyLayers: true,
analyzeModel: false
};
var linesCmdIndex = {};
var prepareGCode = function(){
var percentageTree = undefined;
var prepareGCode = function(totalSize){
if(!lines)return;
gcode = [];
var i, tmp;
var i, tmp, byteCount;
byteCount = 0;
for(i=0;i<lines.length;i++){
// if(lines[i].match(/^(G0|G1|G90|G91|G92|M82|M83|G28)/i))gcode.push(lines[i]);
byteCount += lines[i].length + 1; // line length + \n
tmp = lines[i].indexOf(";");
if(tmp > 1 || tmp === -1) {
gcode.push(lines[i]);
gcode.push({line: lines[i], percentage: byteCount * 100 / totalSize});
}
}
lines = [];
// console.log("GCode prepared");
};
var sortLayers = function(){
var sortedZ = [];
var tmpModel = [];
// var cnt = 0;
// console.log(z_heights);
for(var layer in z_heights){
sortedZ[z_heights[layer]] = layer;
// cnt++;
}
// console.log("cnt is " + cnt);
sortedZ.sort(function(a,b){
return a-b;
});
// console.log(sortedZ);
// console.log(model.length);
for(var i=0;i<sortedZ.length;i++){
// console.log("i is " + i +" and sortedZ[i] is " + sortedZ[i] + "and z_heights[] is " + z_heights[sortedZ[i]] );
if(typeof(z_heights[sortedZ[i]]) === 'undefined')continue;
tmpModel[i] = model[z_heights[sortedZ[i]]];
}
model = tmpModel;
// console.log(model.length);
delete tmpModel;
};
var prepareLinesIndex = function(){
linesCmdIndex = {};
percentageTree = undefined;
for (var l in model){
for (var i=0; i< model[l].length; i++){
linesCmdIndex[model[l][i].gcodeLine] = {layer: l, cmd: i};
for (var l in model) {
for (var i=0; i< model[l].length; i++) {
var percentage = model[l][i].percentage;
var value = {layer: l, cmd: i};
if (!percentageTree) {
percentageTree = new AVLTree({key: percentage, value: value}, "key");
} else {
percentageTree.add({key: percentage, value: value});
}
}
}
}
};
var searchInPercentageTree = function(key) {
if (percentageTree === undefined) {
return undefined;
}
var elements = percentageTree.findBest(key);
if (elements.length == 0) {
return undefined;
}
return elements[0];
};
var purgeLayers = function(){
var purge=true;
@ -102,13 +118,13 @@ GCODE.gCodeReader = (function(){
return {
loadFile: function(reader){
// console.log("loadFile");
model = [];
z_heights = [];
var totalSize = reader.target.result.length;
lines = reader.target.result.split(/\n/);
reader.target.result = null;
prepareGCode();
prepareGCode(totalSize);
worker.postMessage({
"cmd":"parseGCode",
@ -129,32 +145,21 @@ GCODE.gCodeReader = (function(){
}
},
passDataToRenderer: function(){
// console.log(model);
if(gCodeOptions["sortLayers"])sortLayers();
// console.log(model);
if(gCodeOptions["purgeEmptyLayers"])purgeLayers();
prepareLinesIndex();
// console.log(model);
GCODE.renderer.doRender(model, 0);
// GCODE.renderer3d.setModel(model);
},
processLayerFromWorker: function(msg){
// var cmds = msg.cmds;
// var layerNum = msg.layerNum;
// var zHeightObject = msg.zHeightObject;
// var isEmpty = msg.isEmpty;
// console.log(zHeightObject);
model[msg.layerNum] = msg.cmds;
z_heights[msg.zHeightObject.zValue] = msg.zHeightObject.layer;
// GCODE.renderer.doRender(model, msg.layerNum);
},
processMultiLayerFromWorker: function(msg){
for(var i=0;i<msg.layerNum.length;i++){
model[msg.layerNum[i]] = msg.model[msg.layerNum[i]];
z_heights[msg.zHeightObject.zValue[i]] = msg.layerNum[i];
}
// console.log(model);
},
processAnalyzeModelDone: function(msg){
min = msg.min;
@ -190,8 +195,13 @@ GCODE.gCodeReader = (function(){
var result = {first: model[layer][fromSegments].gcodeLine, last: model[layer][toSegments].gcodeLine};
return result;
},
getLinesCmdIndex: function(line){
return linesCmdIndex[line];
getCmdIndexForPercentage: function(percentage) {
var command = searchInPercentageTree(percentage);
if (command === undefined) {
return undefined
} else {
return command.value;
}
}
}
}());

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

View File

@ -0,0 +1,169 @@
// AVLTree ///////////////////////////////////////////////////////////////////
// self file is originally from the Concentré XML project (version 0.2.1)
// Licensed under GPL and LGPL
//
// Modified by Jeremy Stephens.
//
// Taken from: https://gist.github.com/viking/2424106, modified to not only use string literals when searching
// Pass in the attribute you want to use for comparing
function AVLTree(n, attr) {
var self = this;
self.attr = attr;
self.left = null;
self.right = null;
self.node = n;
self.depth = 1;
self.elements = [n];
self.balance = function() {
var ldepth = self.left == null ? 0 : self.left.depth;
var rdepth = self.right == null ? 0 : self.right.depth;
if (ldepth > rdepth + 1) {
// LR or LL rotation
var lldepth = self.left.left == null ? 0 : self.left.left.depth;
var lrdepth = self.left.right == null ? 0 : self.left.right.depth;
if (lldepth < lrdepth) {
// LR rotation consists of a RR rotation of the left child
self.left.rotateRR();
// plus a LL rotation of self node, which happens anyway
}
self.rotateLL();
} else if (ldepth + 1 < rdepth) {
// RR or RL rorarion
var rrdepth = self.right.right == null ? 0 : self.right.right.depth;
var rldepth = self.right.left == null ? 0 : self.right.left.depth;
if (rldepth > rrdepth) {
// RR rotation consists of a LL rotation of the right child
self.right.rotateLL();
// plus a RR rotation of self node, which happens anyway
}
self.rotateRR();
}
}
self.rotateLL = function() {
// the left side is too long => rotate from the left (_not_ leftwards)
var nodeBefore = self.node;
var elementsBefore = self.elements;
var rightBefore = self.right;
self.node = self.left.node;
self.elements = self.left.elements;
self.right = self.left;
self.left = self.left.left;
self.right.left = self.right.right;
self.right.right = rightBefore;
self.right.node = nodeBefore;
self.right.elements = elementsBefore;
self.right.updateInNewLocation();
self.updateInNewLocation();
}
self.rotateRR = function() {
// the right side is too long => rotate from the right (_not_ rightwards)
var nodeBefore = self.node;
var elementsBefore = self.elements;
var leftBefore = self.left;
self.node = self.right.node;
self.elements = self.right.elements;
self.left = self.right;
self.right = self.right.right;
self.left.right = self.left.left;
self.left.left = leftBefore;
self.left.node = nodeBefore;
self.left.elements = elementsBefore;
self.left.updateInNewLocation();
self.updateInNewLocation();
}
self.updateInNewLocation = function() {
self.getDepthFromChildren();
}
self.getDepthFromChildren = function() {
self.depth = self.node == null ? 0 : 1;
if (self.left != null) {
self.depth = self.left.depth + 1;
}
if (self.right != null && self.depth <= self.right.depth) {
self.depth = self.right.depth + 1;
}
}
self.compare = function(n1, n2) {
var v1 = n1[self.attr];
var v2 = n2[self.attr];
if (v1 == v2) {
return 0;
}
if (v1 < v2) {
return -1;
}
return 1;
}
self.add = function(n) {
var o = self.compare(n, self.node);
if (o == 0) {
self.elements.push(n);
return false;
}
var ret = false;
if (o == -1) {
if (self.left == null) {
self.left = new AVLTree(n, self.attr);
ret = true;
} else {
ret = self.left.add(n);
if (ret) {
self.balance();
}
}
} else if (o == 1) {
if (self.right == null) {
self.right = new AVLTree(n, self.attr);
ret = true;
} else {
ret = self.right.add(n);
if (ret) {
self.balance();
}
}
}
if (ret) {
self.getDepthFromChildren();
}
return ret;
}
self.findBest = function(value) {
if (value < self.node[self.attr]) {
if (self.left != null) {
return self.left.findBest(value);
}
} else if (value > self.node[self.attr]) {
if (self.right != null) {
return self.right.findBest(value);
}
}
return self.elements;
}
self.find = function(value) {
var elements = self.findBest(value);
for (var i = 0; i < elements.length; i++) {
if (elements[i][self.attr] == value) {
return elements;
}
}
return false;
}
}

View File

@ -216,21 +216,29 @@ function PrinterStateViewModel(loginStateViewModel) {
self.isSdReady = ko.observable(undefined);
self.filename = ko.observable(undefined);
self.filament = ko.observable(undefined);
self.estimatedPrintTime = ko.observable(undefined);
self.progress = ko.observable(undefined);
self.filesize = ko.observable(undefined);
self.filepos = ko.observable(undefined);
self.printTime = ko.observable(undefined);
self.printTimeLeft = ko.observable(undefined);
self.progress = ko.observable(undefined);
self.currentLine = ko.observable(undefined);
self.totalLines = ko.observable(undefined);
self.sd = ko.observable(undefined);
self.filament = ko.observable(undefined);
self.estimatedPrintTime = ko.observable(undefined);
self.currentHeight = ko.observable(undefined);
self.lineString = ko.computed(function() {
if (!self.totalLines())
self.byteString = ko.computed(function() {
if (!self.filesize())
return "-";
var currentLine = self.currentLine() ? self.currentLine() : "-";
return currentLine + " / " + self.totalLines();
var filepos = self.filepos() ? self.filepos() : "-";
return filepos + " / " + self.filesize();
});
self.heightString = ko.computed(function() {
if (!self.currentHeight())
return "-";
return self.currentHeight();
})
self.progressString = ko.computed(function() {
if (!self.progress())
return 0;
@ -254,8 +262,6 @@ function PrinterStateViewModel(loginStateViewModel) {
self._fromData = function(data) {
self._processStateData(data.state)
self._processJobData(data.job);
self._processGcodeData(data.gcode);
self._processSdUploadData(data.sdUpload);
self._processProgressData(data.progress);
self._processZData(data.currentZ);
}
@ -268,33 +274,15 @@ function PrinterStateViewModel(loginStateViewModel) {
self.isPrinting(data.flags.printing);
self.isError(data.flags.error);
self.isReady(data.flags.ready);
self.isLoading(data.flags.loading);
self.isSdReady(data.flags.sdReady);
}
self._processJobData = function(data) {
self.filename(data.filename);
self.totalLines(data.lines);
self.filesize(data.filesize);
self.estimatedPrintTime(data.estimatedPrintTime);
self.filament(data.filament);
}
self._processGcodeData = function(data) {
if (self.isLoading()) {
var progress = Math.round(data.progress * 100);
if (data.mode == "loading") {
self.filename("Loading... (" + progress + "%)");
} else if (data.mode == "parsing") {
self.filename("Parsing... (" + progress + "%)");
}
}
}
self._processSdUploadData = function(data) {
if (self.isLoading()) {
var progress = Math.round(data.progress * 100);
self.filename("Streaming... (" + progress + "%)");
}
self.sd(data.sd);
}
self._processProgressData = function(data) {
@ -303,7 +291,7 @@ function PrinterStateViewModel(loginStateViewModel) {
} else {
self.progress(undefined);
}
self.currentLine(data.currentLine);
self.filepos(data.filepos);
self.printTime(data.printTime);
self.printTimeLeft(data.printTimeLeft);
}
@ -1093,13 +1081,16 @@ function GcodeViewModel(loginStateViewModel) {
}
self._processData = function(data) {
if(!self.enabled)return;
if (!self.enabled) return;
if (!data.job.filename) return;
if(self.loadedFilename == data.job.filename) {
var cmdIndex = GCODE.gCodeReader.getLinesCmdIndex(data.progress.progress);
if(cmdIndex){
GCODE.renderer.render(cmdIndex.layer, 0, cmdIndex.cmd);
GCODE.ui.updateLayerInfo(cmdIndex.layer);
if(self.loadedFilename && self.loadedFilename == data.job.filename) {
if (data.state.flags && (data.state.flags.printing || data.state.flags.paused)) {
var cmdIndex = GCODE.gCodeReader.getCmdIndexForPercentage(data.progress.progress * 100);
if(cmdIndex){
GCODE.renderer.render(cmdIndex.layer, 0, cmdIndex.cmd);
GCODE.ui.updateLayerInfo(cmdIndex.layer);
}
}
self.errorCount = 0
} else if (data.job.filename) {
@ -1745,43 +1736,48 @@ function ItemListHelper(listType, supportedSorting, supportedFilters, defaultSor
//~~ local storage
self._saveCurrentSortingToLocalStorage = function() {
self._initializeLocalStorage();
var currentSorting = self.currentSorting();
if (currentSorting !== undefined)
localStorage[self.listType + "." + "currentSorting"] = currentSorting;
else
localStorage[self.listType + "." + "currentSorting"] = undefined;
if ( self._initializeLocalStorage() ) {
var currentSorting = self.currentSorting();
if (currentSorting !== undefined)
localStorage[self.listType + "." + "currentSorting"] = currentSorting;
else
localStorage[self.listType + "." + "currentSorting"] = undefined;
}
}
self._loadCurrentSortingFromLocalStorage = function() {
self._initializeLocalStorage();
if (_.contains(_.keys(supportedSorting), localStorage[self.listType + "." + "currentSorting"]))
self.currentSorting(localStorage[self.listType + "." + "currentSorting"]);
else
self.currentSorting(defaultSorting);
if ( self._initializeLocalStorage() ) {
if (_.contains(_.keys(supportedSorting), localStorage[self.listType + "." + "currentSorting"]))
self.currentSorting(localStorage[self.listType + "." + "currentSorting"]);
else
self.currentSorting(defaultSorting);
}
}
self._saveCurrentFiltersToLocalStorage = function() {
self._initializeLocalStorage();
var filters = _.intersection(_.keys(self.supportedFilters), self.currentFilters());
localStorage[self.listType + "." + "currentFilters"] = JSON.stringify(filters);
if ( self._initializeLocalStorage() ) {
var filters = _.intersection(_.keys(self.supportedFilters), self.currentFilters());
localStorage[self.listType + "." + "currentFilters"] = JSON.stringify(filters);
}
}
self._loadCurrentFiltersFromLocalStorage = function() {
self._initializeLocalStorage();
self.currentFilters(_.intersection(_.keys(self.supportedFilters), JSON.parse(localStorage[self.listType + "." + "currentFilters"])));
if ( self._initializeLocalStorage() ) {
self.currentFilters(_.intersection(_.keys(self.supportedFilters), JSON.parse(localStorage[self.listType + "." + "currentFilters"])));
}
}
self._initializeLocalStorage = function() {
if (!Modernizr.localstorage)
return false;
if (localStorage[self.listType + "." + "currentSorting"] !== undefined && localStorage[self.listType + "." + "currentFilters"] !== undefined && JSON.parse(localStorage[self.listType + "." + "currentFilters"]) instanceof Array)
return;
return true;
localStorage[self.listType + "." + "currentSorting"] = self.defaultSorting;
localStorage[self.listType + "." + "currentFilters"] = JSON.stringify(self.defaultFilters);
return true;
}
self._loadCurrentFiltersFromLocalStorage();

View File

@ -108,13 +108,13 @@
<div class="accordion-body collapse in" id="state">
<div class="accordion-inner">
Machine State: <strong data-bind="text: stateString"></strong><br>
File: <strong data-bind="text: filename"></strong><br>
File: <strong data-bind="text: filename"></strong>&nbsp;<strong data-bind="visible: sd">(SD)</strong><br>
Filament: <strong data-bind="text: filament"></strong><br>
Estimated Print Time: <strong data-bind="text: estimatedPrintTime"></strong><br>
Line: <strong data-bind="text: lineString"></strong><br>
Height: <strong data-bind="text: currentHeight"></strong><br>
Height: <strong data-bind="text: heightString"></strong><br>
Print Time: <strong data-bind="text: printTime"></strong><br>
Print Time Left: <strong data-bind="text: printTimeLeft"></strong><br>
Printed: <strong data-bind="text: byteString"></strong><br>
<div class="progress">
<div class="bar" id="job_progressBar" data-bind="style: { width: progressString() + '%' }"></div>
@ -153,7 +153,7 @@
{% if enableSdSupport %}
<div class="sd-trigger accordion-heading-button btn-group">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<span class="icon-hdd"></span>
<span class="icon-sd-black-14"></span>
</a>
<ul class="dropdown-menu">
<li data-bind="visible: !isSdReady()"><a href="#" data-bind="click: function() { $root.initSdCard(); }"><i class="icon-flag"></i> Initialize SD card</a></li>
@ -197,22 +197,22 @@
<div style="display: none;" data-bind="visible: loginState.isUser">
<div class="row-fluid upload-buttons">
{% if enableSdSupport %}
<button class="btn btn-primary fileinput-button span6" data-bind="css: {disabled: !$root.loginState.isUser()}" style="margin-bottom: 10px">
<span class="btn btn-primary fileinput-button span6" data-bind="css: {disabled: !$root.loginState.isUser()}" style="margin-bottom: 10px">
<i class="icon-upload-alt icon-white"></i>
<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()">
</button>
<button class="btn btn-primary fileinput-button span6" data-bind="css: {disabled: !$root.loginState.isUser()}" style="margin-bottom: 10px">
</span>
<span class="btn btn-primary fileinput-button span6" data-bind="css: {disabled: !$root.loginState.isUser()}" style="margin-bottom: 10px">
<i class="icon-upload-alt 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()">
</button>
</span>
{% else %}
<button class="btn btn-primary fileinput-button span12" data-bind="css: {disabled: !$root.loginState.isUser()}" style="margin-bottom: 10px">
<span class="btn btn-primary fileinput-button span12" data-bind="css: {disabled: !$root.loginState.isUser()}" style="margin-bottom: 10px">
<i class="icon-upload-alt icon-white"></i>
<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()">
</button>
</span>
{% endif %}
</div>
<div id="gcode_upload_progress" class="progress" style="width: 100%;">
@ -579,6 +579,7 @@
<script type="text/javascript" src="{{ url_for('static', filename='js/modernizr.custom.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/underscore.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/knockout.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/avltree.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/bootstrap/bootstrap.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/bootstrap/bootstrap-modalmanager.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/bootstrap/bootstrap-modal.js') }}"></script>

View File

@ -16,6 +16,8 @@ import serial
from octoprint.util.avr_isp import stk500v2
from octoprint.util.avr_isp import ispBase
from octoprint.util import matchesGcode
from octoprint.settings import settings
from octoprint.events import eventManager
@ -102,6 +104,7 @@ class VirtualPrinter():
if self._writingToSd and not self._selectedSdFile is None and not "M29" in data:
with open(self._selectedSdFile, "a") as f:
f.write(data)
self.readList.append("ok")
return
#print "Send: %s" % (data.rstrip())
@ -217,6 +220,7 @@ class VirtualPrinter():
self._writingToSd = True
self._selectedSdFile = file
self.readList.append("Writing to file: %s" % filename)
self.readList.append("ok")
def _finishSdFile(self):
@ -314,16 +318,19 @@ class MachineComPrintCallback(object):
def mcZChange(self, newZ):
pass
def mcFileSelected(self, filename, filesize, sd):
pass
def mcSdStateChange(self, sdReady):
pass
def mcSdFiles(self, files):
pass
def mcSdSelected(self, filename, size):
def mcSdPrintingDone(self):
pass
def mcSdPrintingDone(self):
def mcFileTransferStarted(self, filename, filesize):
pass
class MachineCom(object):
@ -366,15 +373,11 @@ class MachineCom(object):
self._bedTemp = 0
self._targetTemp = 0
self._bedTargetTemp = 0
self._gcodeList = None
self._gcodePos = 0
self._commandQueue = queue.Queue()
self._logQueue = queue.Queue(256)
self._feedRateModifier = {}
self._currentZ = -1
self._currentZ = None
self._heatupWaitStartTime = 0
self._heatupWaitTimeLost = 0.0
self._printStartTime = None
self._alwaysSendChecksum = settings().getBoolean(["feature", "alwaysSendChecksum"])
self._currentLine = 1
@ -388,25 +391,21 @@ class MachineCom(object):
self.thread.daemon = True
self.thread.start()
# SD status data
self._sdAvailable = False
self._sdPrinting = False
self._sdFileList = False
self._sdFile = None
self._sdFilePos = None
self._sdFileSize = None
self._sdFiles = []
# print job
self._currentFile = None
def _changeState(self, newState):
if self._state == newState:
return
if newState == self.STATE_CLOSED or newState == self.STATE_CLOSED_WITH_ERROR:
if settings().get(["feature", "sdSupport"]):
self._sdPrinting = False
self._sdFileList = False
self._sdFile = None
self._sdFilePos = None
self._sdFileSize = None
self._sdFiles = []
self._callback.mcSdFiles([])
@ -432,8 +431,10 @@ class MachineCom(object):
if self._state == self.STATE_OPERATIONAL:
return "Operational"
if self._state == self.STATE_PRINTING:
if self._sdPrinting:
if self.isSdFileSelected():
return "Printing from SD"
elif self.isStreaming():
return "Sending file to SD"
else:
return "Printing"
if self._state == self.STATE_PAUSED:
@ -469,53 +470,51 @@ class MachineCom(object):
return self._state == self.STATE_PRINTING
def isSdPrinting(self):
return self._sdPrinting
return self.isSdFileSelected() and self.isPrinting()
def isSdFileSelected(self):
return self._currentFile is not None and isinstance(self._currentFile, PrintingSdFileInformation)
def isStreaming(self):
return self._currentFile is not None and isinstance(self._currentFile, StreamingGcodeFileInformation)
def isPaused(self):
return self._state == self.STATE_PAUSED
def isBusy(self):
return self.isPrinting() or self._state == self.STATE_RECEIVING_FILE
return self.isPrinting() or self.isPaused()
def isSdReady(self):
return self._sdAvailable
def getPrintPos(self):
if self._sdPrinting:
return self._sdFilePos
else:
return self._gcodePos
def getPrintProgress(self):
if self._currentFile is None:
return None
return self._currentFile.getProgress()
def getPrintFilepos(self):
if self._currentFile is None:
return None
return self._currentFile.getFilepos()
def getPrintTime(self):
if self._printStartTime == None:
return 0
if self._currentFile is None or self._currentFile.getStartTime() is None:
return None
else:
return time.time() - self._printStartTime
return time.time() - self._currentFile.getStartTime()
def getPrintTimeRemainingEstimate(self):
if self._printStartTime == None:
printTime = self.getPrintTime()
if printTime is None:
return None
if self._sdPrinting:
printTime = (time.time() - self._printStartTime) / 60
if self._sdFilePos > 0:
printTimeTotal = printTime * self._sdFileSize / self._sdFilePos
else:
printTimeTotal = printTime * self._sdFileSize
printTimeLeft = printTimeTotal - printTime
return printTimeLeft
printTime /= 60
progress = self._currentFile.getProgress()
if progress:
printTimeTotal = printTime / progress
return printTimeTotal - printTime
else:
# for host printing we only start counting the print time at gcode line 100, so we need to calculate stuff
# a bit different here
if self.getPrintPos() < 200:
return None
printTime = (time.time() - self._printStartTime) / 60
printTimeTotal = printTime * (len(self._gcodeList) - 100) / (self.getPrintPos() - 100)
printTimeLeft = printTimeTotal - printTime
return printTimeLeft
def getSdProgress(self):
return (self._sdFilePos, self._sdFileSize)
return None
def getTemp(self):
return self._temp
@ -579,6 +578,7 @@ class MachineCom(object):
tempRequestTimeout = timeout
sdStatusRequestTimeout = timeout
startSeen = not settings().getBoolean(["feature", "waitForStartOnConnect"])
heatingUp = False
while True:
line = self._readline()
if line == None:
@ -614,7 +614,7 @@ class MachineCom(object):
##~~ SD file list
# if we are currently receiving an sd file list, each line is just a filename, so just read it and abort processing
if self._sdFileList and not 'End file list' in line:
self._sdFiles.append(line)
self._sdFiles.append(line.strip().lower())
continue
##~~ Temperature processing
@ -630,16 +630,23 @@ class MachineCom(object):
pass
#If we are waiting for an M109 or M190 then measure the time we lost during heatup, so we can remove that time from our printing time estimate.
if not 'ok' in line and self._heatupWaitStartTime != 0:
t = time.time()
self._heatupWaitTimeLost = t - self._heatupWaitStartTime
self._heatupWaitStartTime = t
if not 'ok' in line:
heatingUp = True
if self._heatupWaitStartTime != 0:
t = time.time()
self._heatupWaitTimeLost = t - self._heatupWaitStartTime
self._heatupWaitStartTime = t
##~~ SD Card handling
elif 'SD init fail' in line:
self._sdAvailable = False
self._sdFiles = []
self._callback.mcSdStateChange(self._sdAvailable)
elif 'Not SD printing' in line:
if self.isSdFileSelected() and self.isPrinting():
# something went wrong, printer is reporting that we actually are not printing right now...
self._sdFilePos = 0
self._changeState(self.STATE_OPERATIONAL)
elif 'SD card ok' in line:
self._sdAvailable = True
self.refreshSdFiles()
@ -653,28 +660,32 @@ class MachineCom(object):
elif 'SD printing byte' in line:
# answer to M27, at least on Marlin, Repetier and Sprinter: "SD printing byte %d/%d"
match = re.search("([0-9]*)/([0-9]*)", line)
self._sdFilePos = int(match.group(1))
self._sdFileSize = int(match.group(2))
self._currentFile.setFilepos(int(match.group(1)))
self._callback.mcProgress()
elif 'File opened' in line:
# answer to M23, at least on Marlin, Repetier and Sprinter: "File opened:%s Size:%d"
match = re.search("File opened:\s*(.*?)\s+Size:\s*([0-9]*)", line)
self._sdFile = match.group(1)
self._sdFileSize = int(match.group(2))
self._currentFile = PrintingSdFileInformation(match.group(1), int(match.group(2)))
elif 'File selected' in line:
# final answer to M23, at least on Marlin, Repetier and Sprinter: "File selected"
self._callback.mcSdSelected(self._sdFile, self._sdFileSize)
self._callback.mcFileSelected(self._currentFile.getFilename(), self._currentFile.getFilesize(), True)
elif 'Writing to file' in line:
# anwer to M28, at least on Marlin, Repetier and Sprinter: "Writing to file: %s"
self._printSection = "CUSTOM"
self._changeState(self.STATE_PRINTING)
elif 'Done printing file' in line:
# printer is reporting file finished printing
self._sdPrinting = False
self._sdFilePos = 0
self._changeState(self.STATE_OPERATIONAL)
self._callback.mcSdPrintingDone()
self._callback.mcPrintjobDone()
##~~ Message handling
elif line.strip() != '' and line.strip() != 'ok' and not line.startswith("wait") and not line.startswith('Resend:') and line != 'echo:Unknown command:""\n' and self.isOperational():
self._callback.mcMessage(line)
if "ok" in line and heatingUp:
heatingUp = False
### Baudrate detection
if self._state == self.STATE_DETECT_BAUDRATE:
if line == '' or time.time() > timeout:
@ -712,6 +723,8 @@ class MachineCom(object):
self._sendCommand("M999")
self._serial.timeout = 2
self._changeState(self.STATE_OPERATIONAL)
if self._sdAvailable:
self.refreshSdFiles()
eventManager().fire("Connected", "%s at %s baud" % (self._port, self._baudrate))
else:
self._testingBaudrate = False
@ -724,6 +737,8 @@ class MachineCom(object):
startSeen = True
elif "ok" in line and startSeen:
self._changeState(self.STATE_OPERATIONAL)
if self._sdAvailable:
self.refreshSdFiles()
eventManager().fire("Connected", "%s at %s baud" % (self._port, self._baudrate))
elif time.time() > timeout:
self.close()
@ -749,12 +764,12 @@ class MachineCom(object):
self._log("Communication timeout during printing, forcing a line")
line = 'ok'
if self._sdPrinting:
if time.time() > tempRequestTimeout:
if self.isSdPrinting():
if time.time() > tempRequestTimeout and not heatingUp:
self._sendCommand("M105")
tempRequestTimeout = time.time() + 5
if time.time() > sdStatusRequestTimeout:
if time.time() > sdStatusRequestTimeout and not heatingUp:
self._sendCommand("M27")
sdStatusRequestTimeout = time.time() + 1
@ -762,7 +777,7 @@ class MachineCom(object):
timeout = time.time() + 5
else:
# Even when printing request the temperature every 5 seconds.
if time.time() > tempRequestTimeout:
if time.time() > tempRequestTimeout and not self.isStreaming():
self._commandQueue.put("M105")
tempRequestTimeout = time.time() + 5
@ -770,7 +785,7 @@ class MachineCom(object):
timeout = time.time() + 5
if self._resendDelta is not None:
self._resendNextCommand()
elif not self._commandQueue.empty():
elif not self._commandQueue.empty() and not self.isStreaming():
self._sendCommand(self._commandQueue.get())
else:
self._sendNext()
@ -864,23 +879,23 @@ class MachineCom(object):
return
for gcode in gcodeToEvent.keys():
if gcode in cmd:
if matchesGcode(cmd, gcode):
eventManager().fire(gcodeToEvent[gcode])
if 'M109' in cmd or 'M190' in cmd:
if matchesGcode(cmd, "M109") or matchesGcode(cmd, "M190"):
self._heatupWaitStartTime = time.time()
if 'M104' in cmd or 'M109' in cmd:
if matchesGcode(cmd, "M104") or matchesGcode(cmd, "M109"):
try:
self._targetTemp = float(re.search('S([0-9]+)', cmd).group(1))
except:
pass
if 'M140' in cmd or 'M190' in cmd:
if matchesGcode(cmd, "M140") or matchesGcode(cmd, "M190"):
try:
self._bedTargetTemp = float(re.search('S([0-9]+)', cmd).group(1))
except:
pass
if "M110" in cmd:
if matchesGcode(cmd, "M110"):
newLineNumber = None
if " N" in cmd:
try:
@ -917,7 +932,7 @@ class MachineCom(object):
if self._alwaysSendChecksum:
lineNumber = self._currentLine
else:
lineNumber = self._gcodePos
lineNumber = self._currentFile.getLineCount()
self._addToLastLines(cmd)
self._currentLine += 1
self._doSendWithChecksum(cmd, lineNumber)
@ -927,8 +942,8 @@ class MachineCom(object):
def _doSendWithChecksum(self, cmd, lineNumber):
self._logger.debug("Sending cmd '%s' with lineNumber %r" % (cmd, lineNumber))
checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (lineNumber, cmd)))
commandToSend = "N%d%s*%d" % (lineNumber, cmd, checksum)
checksum = reduce(lambda x,y:x^y, map(ord, "N%d %s" % (lineNumber, cmd)))
commandToSend = "N%d %s*%d" % (lineNumber, cmd, checksum)
self._doSendWithoutChecksum(commandToSend)
def _doSendWithoutChecksum(self, cmd):
@ -950,131 +965,141 @@ class MachineCom(object):
def _sendNext(self):
with self._sendNextLock:
if self._gcodePos >= len(self._gcodeList):
line = self._currentFile.getNext()
if line is None:
if self.isStreaming():
self._sendCommand("M29")
self._currentFile = None
self._callback.mcFileTransferDone()
else:
self._callback.mcPrintjobDone()
self._changeState(self.STATE_OPERATIONAL)
eventManager().fire('PrintDone')
return
if self._gcodePos == 100:
self._printStartTime100 = time.time()
line = self._gcodeList[self._gcodePos]
if type(line) is tuple:
self._printSection = line[1]
line = line[0]
try:
if line == 'M0' or line == 'M1':
self.setPause(True)
line = 'M105' #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
if self._printSection in self._feedRateModifier:
line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line)
if ('G0' in line or 'G1' in line) and 'Z' in line:
z = float(re.search('Z([0-9\.]*)', line).group(1))
if self._currentZ != z:
self._currentZ = z
self._callback.mcZChange(z)
except:
self._log("Unexpected error: %s" % (getExceptionString()))
if not self.isStreaming():
try:
if matchesGcode(line, "M0") or matchesGcode(line, "M1"):
self.setPause(True)
line = "M105" # Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
if (matchesGcode(line, "G0") or matchesGcode(line, "G1")) and 'Z' in line:
z = float(re.search('Z([0-9\.]*)', line).group(1))
if self._currentZ != z:
self._currentZ = z
self._callback.mcZChange(z)
except:
self._log("Unexpected error: %s" % (getExceptionString()))
self._sendCommand(line, True)
self._gcodePos += 1
self._callback.mcProgress()
def sendCommand(self, cmd):
cmd = cmd.encode('ascii', 'replace')
if self.isPrinting():
if self.isPrinting() and not self.isSdFileSelected():
self._commandQueue.put(cmd)
elif self.isOperational():
self._sendCommand(cmd)
def printGCode(self, gcodeList):
if not self.isOperational() or self.isPrinting():
return
if self._sdPrinting:
self._sdPrinting = False
self._gcodeList = gcodeList
self._gcodePos = 0
self._printSection = 'CUSTOM'
self._changeState(self.STATE_PRINTING)
self._printStartTime = time.time()
self._sendNext()
eventManager().fire("PrintStarted")
def printSdFile(self):
def startPrint(self):
if not self.isOperational() or self.isPrinting():
return
if self.isPaused():
self.sendCommand("M26 S0") # reset position in file to byte 0
self.sendCommand("M24")
if self._currentFile is None:
raise ValueError("No file selected for printing")
self._printSection = 'CUSTOM'
self._sdPrinting = True
self._printSection = "CUSTOM"
self._changeState(self.STATE_PRINTING)
self._printStartTime = time.time()
eventManager().fire("PrintStarted")
try:
self._currentFile.start()
if self.isSdFileSelected():
if self.isPaused():
self.sendCommand("M26 S0")
self._currentFile.setFilepos(0)
self.sendCommand("M24")
else:
self._sendNext()
except:
self._changeState(self.STATE_ERROR)
self._errorValue = getExceptionString()
def startFileTransfer(self, filename, remoteFilename):
if not self.isOperational() or self.isBusy():
return
self._currentFile = StreamingGcodeFileInformation(filename)
self._currentFile.start()
self.sendCommand("M28 %s" % remoteFilename)
self._callback.mcFileTransferStarted(remoteFilename, self._currentFile.getFilesize())
def selectFile(self, filename, sd):
if self.isBusy():
return
if sd:
if not self.isOperational():
# printer is not connected, can't use SD
return
self.sendCommand("M23 %s" % filename)
else:
self._currentFile = PrintingGcodeFileInformation(filename)
self._callback.mcFileSelected(filename, self._currentFile.getFilesize(), False)
def cancelPrint(self):
if self.isOperational():
self._changeState(self.STATE_OPERATIONAL)
if self._sdPrinting:
self._sdPrinting = False
if not self.isOperational() or self.isStreaming():
return
self._changeState(self.STATE_OPERATIONAL)
if self.isSdFileSelected():
self.sendCommand("M25") # pause print
self.sendCommand("M26 S0") # reset position in file to byte 0
eventManager().fire("PrintCancelled")
def setPause(self, pause):
if self.isStreaming():
return
if not pause and self.isPaused():
self._changeState(self.STATE_PRINTING)
if self._sdPrinting:
if self.isSdFileSelected():
self.sendCommand("M24")
else:
for i in xrange(0, 6):
self._sendNext()
self._sendNext()
if pause and self.isPrinting():
self._changeState(self.STATE_PAUSED)
if self._sdPrinting:
if self.isSdFileSelected():
self.sendCommand("M25") # pause print
eventManager().fire("Paused")
def setFeedrateModifier(self, type, value):
self._feedRateModifier[type] = value
def getFeedrateModifiers(self):
result = {}
result.update(self._feedRateModifier)
return result
##~~ SD card
##~~ SD card handling
def getSdFiles(self):
return self._sdFiles
def startSdFileTransfer(self, filename):
if not self.isOperational() or self.isPrinting() or self.isPaused():
if not self.isOperational() or self.isBusy():
return
self._changeState(self.STATE_RECEIVING_FILE)
self.sendCommand("M28 %s" % filename.lower())
def endSdFileTransfer(self, filename):
if not self.isOperational() or self.isPrinting() or self.isPaused():
if not self.isOperational() or self.isBusy():
return
self.sendCommand("M29 %s" % filename.lower())
self._changeState(self.STATE_OPERATIONAL)
self.refreshSdFiles()
def selectSdFile(self, filename):
if not self.isOperational() or self.isPrinting() or self.isPaused():
return
self._sdFile = None
self._sdFilePos = 0
self.sendCommand("M23 %s" % filename.lower())
def deleteSdFile(self, filename):
if not self.isOperational() or ((self.isPrinting() or self.isPaused()) and self._sdFile == filename.lower()):
if not self.isOperational() or (self.isBusy() and self._sdFile == filename.lower()):
# do not delete a file from sd we are currently printing from
return
@ -1082,7 +1107,7 @@ class MachineCom(object):
self.refreshSdFiles()
def refreshSdFiles(self):
if not self.isOperational() or self.isPrinting() or self.isPaused():
if not self.isOperational() or self.isBusy():
return
self.sendCommand("M20")
@ -1092,7 +1117,7 @@ class MachineCom(object):
self.sendCommand("M21")
def releaseSdCard(self):
if not self.isOperational() or ((self.isPrinting() or self.isPaused()) and self._sdPrinting):
if not self.isOperational() or (self.isBusy() and self.isSdFileSelected()):
# do not release the sd card if we are currently printing from it
return
@ -1106,3 +1131,145 @@ class MachineCom(object):
def getExceptionString():
locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
return "%s: '%s' @ %s:%s:%d" % (str(sys.exc_info()[0].__name__), str(sys.exc_info()[1]), os.path.basename(locationInfo[0]), locationInfo[2], locationInfo[1])
class PrintingFileInformation(object):
"""
Encapsulates information regarding the current file being printed: file name, current position, total size and
time the print started.
Allows to reset the current file position to 0 and to calculate the current progress as a floating point
value between 0 and 1.
"""
def __init__(self, filename):
self._filename = filename
self._filepos = 0
self._filesize = None
self._startTime = None
def getStartTime(self):
return self._startTime
def getFilename(self):
return self._filename
def getFilesize(self):
return self._filesize
def getFilepos(self):
return self._filepos
def getProgress(self):
"""
The current progress of the file, calculated as relation between file position and absolute size. Returns -1
if file size is None or < 1.
"""
if self._filesize is None or not self._filesize > 0:
return -1
return float(self._filepos) / float(self._filesize)
def reset(self):
"""
Resets the current file position to 0.
"""
self._filepos = 0
def start(self):
"""
Marks the print job as started and remembers the start time.
"""
self._startTime = time.time()
class PrintingSdFileInformation(PrintingFileInformation):
"""
Encapsulates information regarding an ongoing print from SD.
"""
def __init__(self, filename, filesize):
PrintingFileInformation.__init__(self, filename)
self._filesize = filesize
def setFilepos(self, filepos):
"""
Sets the current file position.
"""
self._filepos = filepos
class PrintingGcodeFileInformation(PrintingFileInformation):
"""
Encapsulates information regarding an ongoing direct print. Takes care of the needed file handle and ensures
that the file is closed in case of an error.
"""
def __init__(self, filename):
PrintingFileInformation.__init__(self, filename)
self._filehandle = None
self._lineCount = 0
self._firstLine = None
self._prevLineType = None
if not os.path.exists(self._filename) or not os.path.isfile(self._filename):
raise IOError("File %s does not exist" % self._filename)
self._filesize = os.stat(self._filename).st_size
def start(self):
"""
Opens the file for reading and determines the file size. Start time won't be recorded until 100 lines in
"""
self._filehandle = open(self._filename, "r")
self._lineCount = 0
self._prevLineType = "CUSTOM"
def getNext(self):
"""
Retrieves the next line for printing.
"""
if self._filehandle is None:
raise ValueError("File %s is not open for reading" % self._filename)
if self._lineCount == 0:
self._lineCount += 1
return "M110 N0"
try:
processedLine = None
while processedLine is None:
if self._filehandle is None:
# file got closed just now
return None
line = self._filehandle.readline()
if not line:
self._filehandle.close()
self._filehandle = None
processedLine = self._processLine(line)
self._lineCount += 1
self._filepos = self._filehandle.tell()
if self._lineCount >= 100 and self._startTime is None:
self._startTime = time.time()
return processedLine
except Exception as (e):
if self._filehandle is not None:
self._filehandle.close()
raise e
def getLineCount(self):
return self._lineCount
def _processLine(self, line):
lineType = self._prevLineType
if line.startswith(";TYPE:"):
lineType = line[6:].strip()
if ";" in line:
line = line[0:line.find(";")]
line = line.strip()
if len(line) > 0:
if self._prevLineType != lineType:
return line, lineType
else:
return line
else:
return None
class StreamingGcodeFileInformation(PrintingGcodeFileInformation):
pass

7
requirements-bbb.txt Normal file
View File

@ -0,0 +1,7 @@
flask==0.9
werkzeug==0.8.3
tornado>=2.4.1
tornadio2>=0.0.4
PyYAML>=3.10
Flask-Login>=0.1.3
Flask-Principal>=0.3.5