Merge branch 'devel' into events
Conflicts: octoprint/printer.py octoprint/server.py octoprint/util/comm.pymaster
commit
ed9e93f379
15
README.md
15
README.md
|
@ -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).
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"])
|
||||
|
|
|
@ -43,7 +43,7 @@ default_settings = {
|
|||
"waitForWaitOnConnect": False,
|
||||
"alwaysSendChecksum": False,
|
||||
"resetLineNumbersWithPrefixedN": False,
|
||||
"sdSupport": False
|
||||
"sdSupport": True
|
||||
},
|
||||
"folder": {
|
||||
"uploads": None,
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -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 |
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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> <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>
|
||||
|
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue