Remove Orig files
This commit is contained in:
parent
4c1fc14ac5
commit
1f3160571c
7 changed files with 1 additions and 3100 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -20,3 +20,4 @@ build
|
|||
dist
|
||||
pypy
|
||||
OctoPrint.egg-info
|
||||
*.orig
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
*.tar.bz2
|
||||
*.tar.gz
|
||||
*.7z
|
||||
*.pyc
|
||||
*.zip
|
||||
*.exe
|
||||
<<<<<<< HEAD
|
||||
*.gcode
|
||||
*.swp
|
||||
darwin-Cura-*
|
||||
win32-Cura-*
|
||||
linux-Cura-*
|
||||
Printrun
|
||||
=======
|
||||
*.iml
|
||||
>>>>>>> devel
|
||||
.idea
|
||||
.DS_Store
|
||||
build
|
||||
dist
|
||||
OctoPrint.egg-info
|
||||
|
|
@ -1,467 +0,0 @@
|
|||
# coding=utf-8
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
|
||||
import os
|
||||
import Queue
|
||||
import threading
|
||||
import datetime
|
||||
import yaml
|
||||
import time
|
||||
import logging
|
||||
import octoprint.util as util
|
||||
import octoprint.util.gcodeInterpreter as gcodeInterpreter
|
||||
from octoprint.settings import settings
|
||||
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
class GcodeManager:
|
||||
def __init__(self):
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
self._settings = settings()
|
||||
|
||||
self._uploadFolder = self._settings.getBaseFolder("uploads")
|
||||
|
||||
self._callbacks = []
|
||||
|
||||
self._metadata = {}
|
||||
self._metadataDirty = False
|
||||
self._metadataFile = os.path.join(self._uploadFolder, "metadata.yaml")
|
||||
self._metadataFileAccessMutex = threading.Lock()
|
||||
|
||||
self._metadataAnalyzer = MetadataAnalyzer(getPathCallback=self.getAbsolutePath, loadedCallback=self._onMetadataAnalysisFinished)
|
||||
|
||||
self._loadMetadata()
|
||||
self._processAnalysisBacklog()
|
||||
|
||||
def _processAnalysisBacklog(self):
|
||||
for osFile in os.listdir(self._uploadFolder):
|
||||
filename = self._getBasicFilename(osFile)
|
||||
absolutePath = self.getAbsolutePath(filename)
|
||||
if absolutePath is None:
|
||||
continue
|
||||
|
||||
fileData = self.getFileData(filename)
|
||||
if fileData is not None and "gcodeAnalysis" in fileData.keys():
|
||||
continue
|
||||
|
||||
self._metadataAnalyzer.addFileToBacklog(filename)
|
||||
|
||||
def _onMetadataAnalysisFinished(self, filename, gcode):
|
||||
if filename is None or gcode is None:
|
||||
return
|
||||
|
||||
basename = os.path.basename(filename)
|
||||
|
||||
absolutePath = self.getAbsolutePath(basename)
|
||||
if absolutePath is None:
|
||||
return
|
||||
|
||||
analysisResult = {}
|
||||
dirty = False
|
||||
if gcode.totalMoveTimeMinute:
|
||||
analysisResult["estimatedPrintTime"] = util.getFormattedTimeDelta(datetime.timedelta(minutes=gcode.totalMoveTimeMinute))
|
||||
dirty = True
|
||||
if gcode.extrusionAmount:
|
||||
analysisResult["filament"] = "%.2fm" % (gcode.extrusionAmount / 1000)
|
||||
if gcode.extrusionVolume:
|
||||
analysisResult["filament"] += " / %.2fcm³" % gcode.extrusionVolume
|
||||
dirty = True
|
||||
|
||||
if dirty:
|
||||
metadata = self.getFileMetadata(basename)
|
||||
metadata["gcodeAnalysis"] = analysisResult
|
||||
self._metadata[basename] = metadata
|
||||
self._metadataDirty = True
|
||||
self._saveMetadata()
|
||||
|
||||
def _loadMetadata(self):
|
||||
if os.path.exists(self._metadataFile) and os.path.isfile(self._metadataFile):
|
||||
with self._metadataFileAccessMutex:
|
||||
with open(self._metadataFile, "r") as f:
|
||||
self._metadata = yaml.safe_load(f)
|
||||
if self._metadata is None:
|
||||
self._metadata = {}
|
||||
|
||||
def _saveMetadata(self, force=False):
|
||||
if not self._metadataDirty and not force:
|
||||
return
|
||||
|
||||
with self._metadataFileAccessMutex:
|
||||
with open(self._metadataFile, "wb") as f:
|
||||
yaml.safe_dump(self._metadata, f, default_flow_style=False, indent=" ", allow_unicode=True)
|
||||
self._metadataDirty = False
|
||||
self._loadMetadata()
|
||||
self._sendUpdateTrigger("gcodeFiles")
|
||||
|
||||
def _getBasicFilename(self, filename):
|
||||
if filename.startswith(self._uploadFolder):
|
||||
return filename[len(self._uploadFolder + os.path.sep):]
|
||||
else:
|
||||
return filename
|
||||
|
||||
#~~ callback handling
|
||||
|
||||
def registerCallback(self, callback):
|
||||
self._callbacks.append(callback)
|
||||
|
||||
def unregisterCallback(self, callback):
|
||||
if callback in self._callbacks:
|
||||
self._callbacks.remove(callback)
|
||||
|
||||
def _sendUpdateTrigger(self, type):
|
||||
for callback in self._callbacks:
|
||||
try: callback.sendUpdateTrigger(type)
|
||||
except: pass
|
||||
|
||||
#~~ file handling
|
||||
|
||||
def addFile(self, file):
|
||||
<<<<<<< HEAD
|
||||
from octoprint.util import isSTLFileName
|
||||
from octoprint.util import isGcodeFileName
|
||||
|
||||
=======
|
||||
>>>>>>> devel
|
||||
if not file:
|
||||
return None
|
||||
|
||||
absolutePath = self.getAbsolutePath(file.filename, mustExist=False)
|
||||
<<<<<<< HEAD
|
||||
|
||||
if absolutePath is None:
|
||||
return None
|
||||
|
||||
file.save(absolutePath)
|
||||
filename = file.filename
|
||||
|
||||
if isGcodeFileName(filename):
|
||||
return self.processGcode(absolutePath)
|
||||
|
||||
curaEnabled = self._settings.get(["curaEngine", "enabled"])
|
||||
|
||||
if isSTLFileName(filename) and curaEnabled:
|
||||
gcodePath = util.genGcodeFileName(absolutePath)
|
||||
logging.info("FILENAME: %s" % filename)
|
||||
|
||||
callBackArgs = [gcodePath]
|
||||
callBack = self.processGcode
|
||||
|
||||
return self.processSTL(
|
||||
filename, absolutePath, callBack, callBackArgs)
|
||||
|
||||
def processSTL(self, filename, absolutePath, callBack, callBackArgs):
|
||||
|
||||
from octoprint.cura import CuraFactory
|
||||
|
||||
curaEngine = CuraFactory.create_slicer()
|
||||
|
||||
gcodePath = util.genGcodeFileName(absolutePath)
|
||||
|
||||
config = self._settings.get(["curaEngine", "config"])
|
||||
|
||||
curaEngine.process_file(
|
||||
config, gcodePath, absolutePath, callBack, callBackArgs)
|
||||
|
||||
def processGcode(self, absolutePath):
|
||||
|
||||
filename = self._getBasicFilename(absolutePath)
|
||||
|
||||
if filename in self._metadata.keys():
|
||||
# delete existing metadata entry, since the file is going to get overwritten
|
||||
del self._metadata[filename]
|
||||
self._metadataDirty = True
|
||||
self._saveMetadata()
|
||||
|
||||
self._metadataAnalyzer.addFileToQueue(os.path.basename(absolutePath))
|
||||
|
||||
return filename
|
||||
=======
|
||||
if absolutePath is None:
|
||||
return None
|
||||
|
||||
basename = self._getBasicFilename(absolutePath)
|
||||
if basename in self._metadata.keys():
|
||||
# delete existing metadata entry, since the file is going to get overwritten
|
||||
del self._metadata[basename]
|
||||
self._metadataDirty = True
|
||||
self._saveMetadata()
|
||||
file.save(absolutePath)
|
||||
self._metadataAnalyzer.addFileToQueue(basename)
|
||||
return basename
|
||||
|
||||
def getFutureFilename(self, file):
|
||||
if not file:
|
||||
return None
|
||||
|
||||
absolutePath = self.getAbsolutePath(file.filename, mustExist=False)
|
||||
if absolutePath is None:
|
||||
return None
|
||||
|
||||
return self._getBasicFilename(absolutePath)
|
||||
>>>>>>> devel
|
||||
|
||||
def removeFile(self, filename):
|
||||
filename = self._getBasicFilename(filename)
|
||||
absolutePath = self.getAbsolutePath(filename)
|
||||
if absolutePath is None:
|
||||
return
|
||||
|
||||
os.remove(absolutePath)
|
||||
if filename in self._metadata.keys():
|
||||
del self._metadata[filename]
|
||||
self._metadataDirty = True
|
||||
self._saveMetadata()
|
||||
|
||||
def getAbsolutePath(self, filename, mustExist=True):
|
||||
"""
|
||||
Returns the absolute path of the given filename in the correct upload folder.
|
||||
|
||||
Ensures that the file
|
||||
<ul>
|
||||
<li>has the extension ".gcode" or ".stl"</li>
|
||||
<li>exists and is a file (not a directory) if "mustExist" is set to True</li>
|
||||
</ul>
|
||||
|
||||
@param filename the name of the file for which to determine the absolute path
|
||||
@param mustExist if set to true, the method also checks if the file exists and is a file
|
||||
@return the absolute path of the file or None if the file is not valid
|
||||
"""
|
||||
filename = self._getBasicFilename(filename)
|
||||
|
||||
if not util.isAllowedFile(filename, set(["gcode", "stl"])):
|
||||
return None
|
||||
|
||||
# TODO: detect which type of file and add in the extra folder portion
|
||||
secure = os.path.join(self._uploadFolder, secure_filename(self._getBasicFilename(filename)))
|
||||
if mustExist and (not os.path.exists(secure) or not os.path.isfile(secure)):
|
||||
return None
|
||||
|
||||
return secure
|
||||
|
||||
def getAllFileData(self):
|
||||
files = []
|
||||
for osFile in os.listdir(self._uploadFolder):
|
||||
fileData = self.getFileData(osFile)
|
||||
if fileData is not None:
|
||||
files.append(fileData)
|
||||
return files
|
||||
|
||||
def getFileData(self, filename):
|
||||
from octoprint.util import isSTLFileName
|
||||
|
||||
if not filename:
|
||||
return
|
||||
|
||||
filename = self._getBasicFilename(filename)
|
||||
|
||||
# TODO: Make this more robust when STLs will be viewable from the client
|
||||
if isSTLFileName(filename):
|
||||
return
|
||||
|
||||
absolutePath = self.getAbsolutePath(filename)
|
||||
if absolutePath is None:
|
||||
return None
|
||||
|
||||
statResult = os.stat(absolutePath)
|
||||
fileData = {
|
||||
"name": filename,
|
||||
"size": util.getFormattedSize(statResult.st_size),
|
||||
"bytes": statResult.st_size,
|
||||
"date": util.getFormattedDateTime(datetime.datetime.fromtimestamp(statResult.st_ctime))
|
||||
}
|
||||
|
||||
# enrich with additional metadata from analysis if available
|
||||
if filename in self._metadata.keys():
|
||||
for key in self._metadata[filename].keys():
|
||||
if key == "prints":
|
||||
val = self._metadata[filename][key]
|
||||
formattedLast = None
|
||||
if val["last"] is not None:
|
||||
formattedLast = {
|
||||
"date": util.getFormattedDateTime(datetime.datetime.fromtimestamp(val["last"]["date"])),
|
||||
"success": val["last"]["success"]
|
||||
}
|
||||
formattedPrints = {
|
||||
"success": val["success"],
|
||||
"failure": val["failure"],
|
||||
"last": formattedLast
|
||||
}
|
||||
fileData["prints"] = formattedPrints
|
||||
else:
|
||||
fileData[key] = self._metadata[filename][key]
|
||||
|
||||
return fileData
|
||||
|
||||
def getFileMetadata(self, filename):
|
||||
filename = self._getBasicFilename(filename)
|
||||
if filename in self._metadata.keys():
|
||||
return self._metadata[filename]
|
||||
else:
|
||||
return {
|
||||
"prints": {
|
||||
"success": 0,
|
||||
"failure": 0,
|
||||
"last": None
|
||||
}
|
||||
}
|
||||
|
||||
def setFileMetadata(self, filename, metadata):
|
||||
filename = self._getBasicFilename(filename)
|
||||
self._metadata[filename] = metadata
|
||||
self._metadataDirty = True
|
||||
|
||||
#~~ print job data
|
||||
|
||||
def printSucceeded(self, filename):
|
||||
filename = self._getBasicFilename(filename)
|
||||
absolutePath = self.getAbsolutePath(filename)
|
||||
if absolutePath is None:
|
||||
return
|
||||
|
||||
metadata = self.getFileMetadata(filename)
|
||||
metadata["prints"]["success"] += 1
|
||||
metadata["prints"]["last"] = {
|
||||
"date": time.time(),
|
||||
"success": True
|
||||
}
|
||||
self.setFileMetadata(filename, metadata)
|
||||
self._saveMetadata()
|
||||
|
||||
def printFailed(self, filename):
|
||||
filename = self._getBasicFilename(filename)
|
||||
absolutePath = self.getAbsolutePath(filename)
|
||||
if absolutePath is None:
|
||||
return
|
||||
|
||||
metadata = self.getFileMetadata(filename)
|
||||
metadata["prints"]["failure"] += 1
|
||||
metadata["prints"]["last"] = {
|
||||
"date": time.time(),
|
||||
"success": False
|
||||
}
|
||||
self.setFileMetadata(filename, metadata)
|
||||
self._saveMetadata()
|
||||
|
||||
def changeLastPrintSuccess(self, filename, succeeded):
|
||||
filename = self._getBasicFilename(filename)
|
||||
absolutePath = self.getAbsolutePath(filename)
|
||||
if absolutePath is None:
|
||||
return
|
||||
|
||||
metadata = self.getFileMetadata(filename)
|
||||
if metadata is None:
|
||||
return
|
||||
|
||||
if "prints" in metadata.keys():
|
||||
if "last" in metadata.keys() and metadata["prints"]["last"] is not None:
|
||||
currentSucceeded = metadata["prints"]["last"]["success"]
|
||||
if currentSucceeded != succeeded:
|
||||
metadata["prints"]["last"]["success"] = succeeded
|
||||
if currentSucceeded:
|
||||
# last print job was counted as success but actually failed
|
||||
metadata["prints"]["success"] -= 1
|
||||
metadata["prints"]["failure"] += 1
|
||||
else:
|
||||
# last print job was counted as a failure but actually succeeded
|
||||
metadata["prints"]["success"] += 1
|
||||
metadata["prints"]["failure"] -= 1
|
||||
self.setFileMetadata(filename, metadata)
|
||||
self._saveMetadata()
|
||||
|
||||
#~~ analysis control
|
||||
|
||||
def pauseAnalysis(self):
|
||||
self._metadataAnalyzer.pause()
|
||||
|
||||
def resumeAnalysis(self):
|
||||
self._metadataAnalyzer.resume()
|
||||
|
||||
class MetadataAnalyzer:
|
||||
def __init__(self, getPathCallback, loadedCallback):
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
self._getPathCallback = getPathCallback
|
||||
self._loadedCallback = loadedCallback
|
||||
|
||||
self._active = threading.Event()
|
||||
self._active.set()
|
||||
|
||||
self._currentFile = None
|
||||
self._currentProgress = None
|
||||
|
||||
self._queue = Queue.PriorityQueue()
|
||||
self._gcode = None
|
||||
|
||||
self._worker = threading.Thread(target=self._work)
|
||||
self._worker.daemon = True
|
||||
self._worker.start()
|
||||
|
||||
def addFileToQueue(self, filename):
|
||||
self._logger.debug("Adding file %s to analysis queue (high priority)" % filename)
|
||||
self._queue.put((0, filename))
|
||||
|
||||
def addFileToBacklog(self, filename):
|
||||
self._logger.debug("Adding file %s to analysis backlog (low priority)" % filename)
|
||||
self._queue.put((100, filename))
|
||||
|
||||
def working(self):
|
||||
return self.isActive() and not (self._queue.empty() and self._currentFile is None)
|
||||
|
||||
def isActive(self):
|
||||
return self._active.is_set()
|
||||
|
||||
def pause(self):
|
||||
self._logger.debug("Pausing Gcode analyzer")
|
||||
self._active.clear()
|
||||
if self._gcode is not None:
|
||||
self._logger.debug("Aborting running analysis, will restart when Gcode analyzer is resumed")
|
||||
self._gcode.abort()
|
||||
|
||||
def resume(self):
|
||||
self._logger.debug("Resuming Gcode analyzer")
|
||||
self._active.set()
|
||||
|
||||
def _work(self):
|
||||
aborted = None
|
||||
while True:
|
||||
self._active.wait()
|
||||
|
||||
if aborted is not None:
|
||||
filename = aborted
|
||||
aborted = None
|
||||
self._logger.debug("Got an aborted analysis job for file %s, processing this instead of first item in queue" % filename)
|
||||
else:
|
||||
(priority, filename) = self._queue.get()
|
||||
self._logger.debug("Processing file %s from queue (priority %d)" % (filename, priority))
|
||||
|
||||
try:
|
||||
self._analyzeGcode(filename)
|
||||
self._queue.task_done()
|
||||
except gcodeInterpreter.AnalysisAborted:
|
||||
aborted = filename
|
||||
self._logger.debug("Running analysis of file %s aborted" % filename)
|
||||
|
||||
def _analyzeGcode(self, filename):
|
||||
path = self._getPathCallback(filename)
|
||||
if path is None:
|
||||
return
|
||||
|
||||
self._currentFile = filename
|
||||
self._currentProgress = 0
|
||||
|
||||
try:
|
||||
self._logger.debug("Starting analysis of file %s" % filename)
|
||||
self._gcode = gcodeInterpreter.gcode()
|
||||
self._gcode.progressCallback = self._onParsingProgress
|
||||
self._gcode.load(path)
|
||||
self._logger.debug("Analysis of file %s finished, notifying callback" % filename)
|
||||
self._loadedCallback(self._currentFile, self._gcode)
|
||||
finally:
|
||||
self._gcode = None
|
||||
self._currentProgress = None
|
||||
self._currentFile = None
|
||||
|
||||
def _onParsingProgress(self, progress):
|
||||
self._currentProgress = progress
|
||||
|
|
@ -1,719 +0,0 @@
|
|||
# coding=utf-8
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
|
||||
import time
|
||||
import datetime
|
||||
import threading
|
||||
import copy
|
||||
import os
|
||||
|
||||
#import logging, logging.config
|
||||
|
||||
import octoprint.util.comm as comm
|
||||
import octoprint.util as util
|
||||
|
||||
from octoprint.settings import settings
|
||||
from octoprint.events import eventManager
|
||||
|
||||
def getConnectionOptions():
|
||||
"""
|
||||
Retrieves the available ports, baudrates, prefered port and baudrate for connecting to the printer.
|
||||
"""
|
||||
return {
|
||||
"ports": comm.serialList(),
|
||||
"baudrates": comm.baudrateList(),
|
||||
"portPreference": settings().get(["serial", "port"]),
|
||||
"baudratePreference": settings().getInt(["serial", "baudrate"]),
|
||||
"autoconnect": settings().getBoolean(["serial", "autoconnect"])
|
||||
}
|
||||
|
||||
class Printer():
|
||||
def __init__(self, gcodeManager):
|
||||
from collections import deque
|
||||
|
||||
self._gcodeManager = gcodeManager
|
||||
self._gcodeManager.registerCallback(self)
|
||||
|
||||
# state
|
||||
self._temp = None
|
||||
self._bedTemp = None
|
||||
self._targetTemp = None
|
||||
self._targetBedTemp = None
|
||||
self._temps = {
|
||||
"actual": deque([], 300),
|
||||
"target": deque([], 300),
|
||||
"actualBed": deque([], 300),
|
||||
"targetBed": deque([], 300)
|
||||
}
|
||||
self._tempBacklog = []
|
||||
|
||||
self._latestMessage = None
|
||||
self._messages = deque([], 300)
|
||||
self._messageBacklog = []
|
||||
|
||||
self._latestLog = None
|
||||
self._log = deque([], 300)
|
||||
self._logBacklog = []
|
||||
|
||||
self._state = None
|
||||
|
||||
self._currentZ = None
|
||||
|
||||
self._progress = None
|
||||
self._printTime = None
|
||||
self._printTimeLeft = None
|
||||
|
||||
self._printAfterSelect = False
|
||||
|
||||
# sd handling
|
||||
self._sdPrinting = False
|
||||
self._sdStreaming = False
|
||||
|
||||
self._selectedFile = None
|
||||
|
||||
# comm
|
||||
self._comm = None
|
||||
|
||||
# callbacks
|
||||
self._callbacks = []
|
||||
self._lastProgressReport = None
|
||||
|
||||
self._stateMonitor = StateMonitor(
|
||||
ratelimit=0.5,
|
||||
updateCallback=self._sendCurrentDataCallbacks,
|
||||
addTemperatureCallback=self._sendAddTemperatureCallbacks,
|
||||
addLogCallback=self._sendAddLogCallbacks,
|
||||
addMessageCallback=self._sendAddMessageCallbacks
|
||||
)
|
||||
self._stateMonitor.reset(
|
||||
state={"state": None, "stateString": self.getStateString(), "flags": self._getStateFlags()},
|
||||
jobData={"filename": None, "filesize": None, "estimatedPrintTime": None, "filament": None},
|
||||
progress={"progress": None, "filepos": None, "printTime": None, "printTimeLeft": None},
|
||||
currentZ=None
|
||||
)
|
||||
|
||||
#~~ callback handling
|
||||
|
||||
def registerCallback(self, callback):
|
||||
self._callbacks.append(callback)
|
||||
self._sendInitialStateUpdate(callback)
|
||||
|
||||
def unregisterCallback(self, callback):
|
||||
if callback in self._callbacks:
|
||||
self._callbacks.remove(callback)
|
||||
|
||||
def _sendAddTemperatureCallbacks(self, data):
|
||||
for callback in self._callbacks:
|
||||
try: callback.addTemperature(data)
|
||||
except: pass
|
||||
|
||||
def _sendAddLogCallbacks(self, data):
|
||||
for callback in self._callbacks:
|
||||
try: callback.addLog(data)
|
||||
except: pass
|
||||
|
||||
def _sendAddMessageCallbacks(self, data):
|
||||
for callback in self._callbacks:
|
||||
try: callback.addMessage(data)
|
||||
except: pass
|
||||
|
||||
def _sendCurrentDataCallbacks(self, data):
|
||||
for callback in self._callbacks:
|
||||
try: callback.sendCurrentData(copy.deepcopy(data))
|
||||
except: pass
|
||||
|
||||
def _sendTriggerUpdateCallbacks(self, type):
|
||||
for callback in self._callbacks:
|
||||
try: callback.sendUpdateTrigger(type)
|
||||
except: pass
|
||||
|
||||
def _sendFeedbackCommandOutput(self, name, output):
|
||||
for callback in self._callbacks:
|
||||
try: callback.sendFeedbackCommandOutput(name, output)
|
||||
except: pass
|
||||
|
||||
#~~ callback from gcodemanager
|
||||
|
||||
def sendUpdateTrigger(self, type):
|
||||
if type == "gcodeFiles" and self._selectedFile:
|
||||
self._setJobData(self._selectedFile["filename"],
|
||||
self._selectedFile["filesize"],
|
||||
self._selectedFile["sd"])
|
||||
|
||||
#~~ printer commands
|
||||
|
||||
def connect(self, port=None, baudrate=None):
|
||||
"""
|
||||
Connects to the printer. If port and/or baudrate is provided, uses these settings, otherwise autodetection
|
||||
will be attempted.
|
||||
"""
|
||||
if self._comm is not None:
|
||||
self._comm.close()
|
||||
self._comm = comm.MachineCom(port, baudrate, callbackObject=self)
|
||||
|
||||
def disconnect(self):
|
||||
"""
|
||||
Closes the connection to the printer.
|
||||
"""
|
||||
if self._comm is not None:
|
||||
self._comm.close()
|
||||
self._comm = None
|
||||
eventManager().fire("Disconnected")
|
||||
|
||||
def command(self, command):
|
||||
"""
|
||||
Sends a single gcode command to the printer.
|
||||
"""
|
||||
self.commands([command])
|
||||
|
||||
def commands(self, commands):
|
||||
"""
|
||||
Sends multiple gcode commands (provided as a list) to the printer.
|
||||
"""
|
||||
for command in commands:
|
||||
self._comm.sendCommand(command)
|
||||
|
||||
def selectFile(self, filename, sd, printAfterSelect=False):
|
||||
if self._comm is not None and (self._comm.isBusy() or self._comm.isStreaming()):
|
||||
return
|
||||
|
||||
self._printAfterSelect = printAfterSelect
|
||||
self._comm.selectFile(filename, sd)
|
||||
self._setProgressData(0, None, None, None)
|
||||
self._setCurrentZ(None)
|
||||
|
||||
def unselectFile(self):
|
||||
if self._comm is not None and (self._comm.isBusy() or self._comm.isStreaming()):
|
||||
return
|
||||
|
||||
self._comm.unselectFile()
|
||||
self._setProgressData(0, None, None, None)
|
||||
self._setCurrentZ(None)
|
||||
|
||||
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() or self._comm.isPrinting():
|
||||
return
|
||||
if self._selectedFile is None:
|
||||
return
|
||||
|
||||
self._setCurrentZ(None)
|
||||
self._comm.startPrint()
|
||||
|
||||
def togglePausePrint(self):
|
||||
"""
|
||||
Pause the current printjob.
|
||||
"""
|
||||
if self._comm is None:
|
||||
return
|
||||
|
||||
self._comm.setPause(not self._comm.isPaused())
|
||||
|
||||
def cancelPrint(self, disableMotorsAndHeater=True):
|
||||
"""
|
||||
Cancel the current printjob.
|
||||
"""
|
||||
if self._comm is None:
|
||||
return
|
||||
|
||||
self._comm.cancelPrint()
|
||||
|
||||
if disableMotorsAndHeater:
|
||||
self.commands(["M84", "M104 S0", "M140 S0", "M106 S0"]) # disable motors, switch off heaters and fan
|
||||
|
||||
# reset progress, height, print time
|
||||
self._setCurrentZ(None)
|
||||
self._setProgressData(None, None, None, None)
|
||||
|
||||
# mark print as failure
|
||||
if self._selectedFile is not None:
|
||||
self._gcodeManager.printFailed(self._selectedFile["filename"])
|
||||
eventManager().fire("PrintFailed", self._selectedFile["filename"])
|
||||
|
||||
#~~ state monitoring
|
||||
|
||||
def _setCurrentZ(self, currentZ):
|
||||
self._currentZ = currentZ
|
||||
|
||||
formattedCurrentZ = None
|
||||
if self._currentZ:
|
||||
formattedCurrentZ = "%.2f mm" % (self._currentZ)
|
||||
self._stateMonitor.setCurrentZ(formattedCurrentZ)
|
||||
|
||||
def _setState(self, state):
|
||||
self._state = state
|
||||
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
|
||||
|
||||
def _addLog(self, log):
|
||||
self._log.append(log)
|
||||
self._stateMonitor.addLog(log)
|
||||
|
||||
def _addMessage(self, message):
|
||||
self._messages.append(message)
|
||||
self._stateMonitor.addMessage(message)
|
||||
|
||||
def _setProgressData(self, progress, filepos, printTime, printTimeLeft):
|
||||
self._progress = progress
|
||||
self._printTime = printTime
|
||||
self._printTimeLeft = printTimeLeft
|
||||
|
||||
formattedPrintTime = None
|
||||
if (self._printTime):
|
||||
formattedPrintTime = util.getFormattedTimeDelta(datetime.timedelta(seconds=self._printTime))
|
||||
|
||||
formattedPrintTimeLeft = None
|
||||
if (self._printTimeLeft):
|
||||
formattedPrintTimeLeft = util.getFormattedTimeDelta(datetime.timedelta(minutes=self._printTimeLeft))
|
||||
|
||||
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)
|
||||
|
||||
self._temps["actual"].append((currentTimeUtc, temp))
|
||||
self._temps["target"].append((currentTimeUtc, targetTemp))
|
||||
self._temps["actualBed"].append((currentTimeUtc, bedTemp))
|
||||
self._temps["targetBed"].append((currentTimeUtc, bedTargetTemp))
|
||||
|
||||
self._temp = temp
|
||||
self._bedTemp = bedTemp
|
||||
self._targetTemp = targetTemp
|
||||
self._targetBedTemp = bedTargetTemp
|
||||
|
||||
self._stateMonitor.addTemperature({"currentTime": currentTimeUtc, "temp": self._temp, "bedTemp": self._bedTemp, "targetTemp": self._targetTemp, "targetBedTemp": self._targetBedTemp})
|
||||
|
||||
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
|
||||
fileMTime = None
|
||||
filament = None
|
||||
if filename:
|
||||
formattedFilename = os.path.basename(filename)
|
||||
|
||||
# Use a string for mtime because it could be float and the
|
||||
# javascript needs to exact match
|
||||
if not sd:
|
||||
fileMTime = str(os.stat(filename).st_mtime)
|
||||
|
||||
if filesize:
|
||||
formattedFilesize = util.getFormattedSize(filesize)
|
||||
|
||||
fileData = self._gcodeManager.getFileData(filename)
|
||||
if fileData is not None and "gcodeAnalysis" in fileData.keys():
|
||||
if "estimatedPrintTime" in fileData["gcodeAnalysis"].keys():
|
||||
estimatedPrintTime = fileData["gcodeAnalysis"]["estimatedPrintTime"]
|
||||
if "filament" in fileData["gcodeAnalysis"].keys():
|
||||
filament = fileData["gcodeAnalysis"]["filament"]
|
||||
|
||||
self._stateMonitor.setJobData({"filename": formattedFilename, "filesize": formattedFilesize, "estimatedPrintTime": estimatedPrintTime, "filament": filament, "sd": sd, "mtime": fileMTime})
|
||||
|
||||
def _sendInitialStateUpdate(self, callback):
|
||||
try:
|
||||
data = self._stateMonitor.getCurrentData()
|
||||
# convert the dict of deques to a dict of lists
|
||||
temps = {k: list(v) for (k,v) in self._temps.iteritems()}
|
||||
data.update({
|
||||
"temperatureHistory": temps,
|
||||
"logHistory": list(self._log),
|
||||
"messageHistory": list(self._messages)
|
||||
})
|
||||
callback.sendHistoryData(data)
|
||||
except Exception, err:
|
||||
import sys
|
||||
sys.stderr.write("ERROR: %s\n" % str(err))
|
||||
pass
|
||||
|
||||
def _getStateFlags(self):
|
||||
if not settings().getBoolean(["feature", "sdSupport"]) or self._comm is None:
|
||||
sdReady = False
|
||||
else:
|
||||
sdReady = self._comm.isSdReady()
|
||||
|
||||
return {
|
||||
"operational": self.isOperational(),
|
||||
"printing": self.isPrinting(),
|
||||
"closedOrError": self.isClosedOrError(),
|
||||
"error": self.isError(),
|
||||
"paused": self.isPaused(),
|
||||
"ready": self.isReady(),
|
||||
"sdReady": sdReady
|
||||
}
|
||||
|
||||
def getCurrentData(self):
|
||||
return self._stateMonitor.getCurrentData()
|
||||
|
||||
#~~ callbacks triggered from self._comm
|
||||
|
||||
def mcLog(self, message):
|
||||
"""
|
||||
Callback method for the comm object, called upon log output.
|
||||
"""
|
||||
self._addLog(message)
|
||||
|
||||
def mcTempUpdate(self, temp, bedTemp, targetTemp, bedTargetTemp):
|
||||
self._addTemperatureData(temp, bedTemp, targetTemp, bedTargetTemp)
|
||||
|
||||
def mcStateChange(self, state):
|
||||
"""
|
||||
Callback method for the comm object, called if the connection state changes.
|
||||
"""
|
||||
oldState = self._state
|
||||
|
||||
# forward relevant state changes to gcode manager
|
||||
if self._comm is not None and oldState == self._comm.STATE_PRINTING:
|
||||
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
|
||||
|
||||
self._setState(state)
|
||||
|
||||
def mcMessage(self, message):
|
||||
"""
|
||||
Callback method for the comm object, called upon message exchanges via serial.
|
||||
Stores the message in the message buffer, truncates buffer to the last 300 lines.
|
||||
"""
|
||||
self._addMessage(message)
|
||||
|
||||
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 progress.
|
||||
"""
|
||||
|
||||
self._setProgressData(self._comm.getPrintProgress(), self._comm.getPrintFilepos(), self._comm.getPrintTime(), self._comm.getPrintTimeRemainingEstimate())
|
||||
|
||||
def mcZChange(self, newZ):
|
||||
"""
|
||||
Callback method for the comm object, called upon change of the z-layer.
|
||||
"""
|
||||
oldZ = self._currentZ
|
||||
if newZ != oldZ:
|
||||
# we have to react to all z-changes, even those that might "go backward" due to a slicer's retraction or
|
||||
# anti-backlash-routines. Event subscribes should individually take care to filter out "wrong" z-changes
|
||||
eventManager().fire("ZChange", newZ)
|
||||
|
||||
self._setCurrentZ(newZ)
|
||||
|
||||
def mcSdStateChange(self, sdReady):
|
||||
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
|
||||
|
||||
def mcSdFiles(self, files):
|
||||
self._sendTriggerUpdateCallbacks("gcodeFiles")
|
||||
|
||||
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._printAfterSelect:
|
||||
self.startPrint()
|
||||
|
||||
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()})
|
||||
|
||||
def mcFileTransferStarted(self, filename, filesize):
|
||||
self._sdStreaming = 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._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()})
|
||||
|
||||
def mcReceivedRegisteredMessage(self, command, output):
|
||||
self._sendFeedbackCommandOutput(command, output)
|
||||
|
||||
#~~ sd file handling
|
||||
|
||||
def getSdFiles(self):
|
||||
if self._comm is None:
|
||||
return
|
||||
return self._comm.getSdFiles()
|
||||
|
||||
<<<<<<< HEAD
|
||||
def addSdFile(self, filename, absolutePath):
|
||||
from octoprint.util import isGcodeFileName
|
||||
from octoprint.util import isSTLFileName
|
||||
|
||||
if not self._comm:
|
||||
return
|
||||
|
||||
if isGcodeFileName(filename):
|
||||
self.streamSdFile(filename, absolutePath)
|
||||
|
||||
if isSTLFileName(filename):
|
||||
gcodePath = util.genGcodeFileName(absolutePath)
|
||||
callBackArgs = [filename, absolutePath]
|
||||
callBack = self.streamSdFile
|
||||
|
||||
gcodeManager = GcodeManager()
|
||||
gcodeManager.processSTL(
|
||||
filename, absolutePath, callBack, callBackArgs)
|
||||
|
||||
def streamSdFile(filename, absolutePath):
|
||||
self._sdStreamer = SdFileStreamer(self._comm, filename, absolutePath, self._onSdFileStreamProgress, self._onSdFileStreamFinish)
|
||||
self._sdStreamer.start()
|
||||
logging.info("Stream file to SD started")
|
||||
=======
|
||||
def addSdFile(self, filename, path):
|
||||
if not self._comm or self._comm.isBusy():
|
||||
return
|
||||
self._comm.startFileTransfer(path, filename[:8].lower() + ".gco")
|
||||
>>>>>>> devel
|
||||
|
||||
def deleteSdFile(self, filename):
|
||||
if not self._comm:
|
||||
return
|
||||
self._comm.deleteSdFile(filename)
|
||||
|
||||
def initSdCard(self):
|
||||
if not self._comm:
|
||||
return
|
||||
self._comm.initSdCard()
|
||||
|
||||
def releaseSdCard(self):
|
||||
if not self._comm:
|
||||
return
|
||||
self._comm.releaseSdCard()
|
||||
|
||||
def refreshSdFiles(self):
|
||||
if not self._comm:
|
||||
return
|
||||
self._comm.refreshSdFiles()
|
||||
|
||||
#~~ state reports
|
||||
|
||||
def getStateString(self):
|
||||
"""
|
||||
Returns a human readable string corresponding to the current communication state.
|
||||
"""
|
||||
if self._comm is None:
|
||||
return "Offline"
|
||||
else:
|
||||
return self._comm.getStateString()
|
||||
|
||||
def getCurrentData(self):
|
||||
return self._stateMonitor.getCurrentData()
|
||||
|
||||
def getCurrentJob(self):
|
||||
currentData = self._stateMonitor.getCurrentData()
|
||||
return currentData["job"]
|
||||
|
||||
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()
|
||||
|
||||
def isOperational(self):
|
||||
return self._comm is not None and self._comm.isOperational()
|
||||
|
||||
def isPrinting(self):
|
||||
return self._comm is not None and self._comm.isPrinting()
|
||||
|
||||
def isPaused(self):
|
||||
return self._comm is not None and self._comm.isPaused()
|
||||
|
||||
def isError(self):
|
||||
return self._comm is not None and self._comm.isError()
|
||||
|
||||
def isReady(self):
|
||||
return self.isOperational() and not self._comm.isStreaming()
|
||||
|
||||
def isLoading(self):
|
||||
return self._gcodeLoader is not None
|
||||
|
||||
class GcodeLoader(threading.Thread):
|
||||
"""
|
||||
The GcodeLoader takes care of loading a gcode-File from disk and parsing it into a gcode object in a separate
|
||||
thread while constantly notifying interested listeners about the current progress.
|
||||
The progress is returned as a float value between 0 and 1 which is to be interpreted as the percentage of completion.
|
||||
"""
|
||||
|
||||
def __init__(self, filename, progressCallback, loadedCallback):
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
self._progressCallback = progressCallback
|
||||
self._loadedCallback = loadedCallback
|
||||
|
||||
self._filename = filename
|
||||
self._gcodeList = None
|
||||
|
||||
def run(self):
|
||||
#Send an initial M110 to reset the line counter to zero.
|
||||
prevLineType = lineType = "CUSTOM"
|
||||
gcodeList = ["M110 N0"]
|
||||
filesize = os.stat(self._filename).st_size
|
||||
with open(self._filename, "r") as file:
|
||||
for line in file:
|
||||
if line.startswith(";TYPE:"):
|
||||
lineType = line[6:].strip()
|
||||
if ";" in line:
|
||||
line = line[0:line.find(";")]
|
||||
line = line.strip()
|
||||
if len(line) > 0:
|
||||
if prevLineType != lineType:
|
||||
gcodeList.append((line, lineType, ))
|
||||
else:
|
||||
gcodeList.append(line)
|
||||
prevLineType = lineType
|
||||
self._onLoadingProgress(float(file.tell()) / float(filesize))
|
||||
|
||||
self._gcodeList = gcodeList
|
||||
self._loadedCallback(self._filename, self._gcodeList)
|
||||
|
||||
def _onLoadingProgress(self, progress):
|
||||
self._progressCallback(self._filename, progress, "loading")
|
||||
|
||||
def _onParsingProgress(self, progress):
|
||||
self._progressCallback(self._filename, progress, "parsing")
|
||||
|
||||
class SdFileStreamer(threading.Thread):
|
||||
def __init__(self, comm, filename, file, progressCallback, finishCallback):
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
self._comm = comm
|
||||
self._filename = filename
|
||||
self._file = file
|
||||
self._progressCallback = progressCallback
|
||||
self._finishCallback = finishCallback
|
||||
|
||||
def run(self):
|
||||
if self._comm.isBusy():
|
||||
return
|
||||
|
||||
name = self._filename[:self._filename.rfind(".")]
|
||||
sdFilename = name[:8].lower() + ".gco"
|
||||
try:
|
||||
size = os.stat(self._file).st_size
|
||||
with open(self._file, "r") as f:
|
||||
self._comm.startSdFileTransfer(sdFilename)
|
||||
for line in f:
|
||||
if ";" in line:
|
||||
line = line[0:line.find(";")]
|
||||
line = line.strip()
|
||||
if len(line) > 0:
|
||||
self._comm.sendCommand(line)
|
||||
time.sleep(0.001) # do not send too fast
|
||||
self._progressCallback(sdFilename, float(f.tell()) / float(size))
|
||||
finally:
|
||||
self._comm.endSdFileTransfer(sdFilename)
|
||||
self._finishCallback(sdFilename)
|
||||
|
||||
class StateMonitor(object):
|
||||
def __init__(self, ratelimit, updateCallback, addTemperatureCallback, addLogCallback, addMessageCallback):
|
||||
self._ratelimit = ratelimit
|
||||
self._updateCallback = updateCallback
|
||||
self._addTemperatureCallback = addTemperatureCallback
|
||||
self._addLogCallback = addLogCallback
|
||||
self._addMessageCallback = addMessageCallback
|
||||
|
||||
self._state = None
|
||||
self._jobData = None
|
||||
self._gcodeData = None
|
||||
self._sdUploadData = None
|
||||
self._currentZ = None
|
||||
self._progress = None
|
||||
|
||||
self._changeEvent = threading.Event()
|
||||
|
||||
self._lastUpdate = time.time()
|
||||
self._worker = threading.Thread(target=self._work)
|
||||
self._worker.daemon = True
|
||||
self._worker.start()
|
||||
|
||||
def reset(self, state=None, jobData=None, progress=None, currentZ=None):
|
||||
self.setState(state)
|
||||
self.setJobData(jobData)
|
||||
self.setProgress(progress)
|
||||
self.setCurrentZ(currentZ)
|
||||
|
||||
def addTemperature(self, temperature):
|
||||
self._addTemperatureCallback(temperature)
|
||||
self._changeEvent.set()
|
||||
|
||||
def addLog(self, log):
|
||||
self._addLogCallback(log)
|
||||
self._changeEvent.set()
|
||||
|
||||
def addMessage(self, message):
|
||||
self._addMessageCallback(message)
|
||||
self._changeEvent.set()
|
||||
|
||||
def setCurrentZ(self, currentZ):
|
||||
self._currentZ = currentZ
|
||||
self._changeEvent.set()
|
||||
|
||||
def setState(self, state):
|
||||
self._state = state
|
||||
self._changeEvent.set()
|
||||
|
||||
def setJobData(self, jobData):
|
||||
self._jobData = jobData
|
||||
self._changeEvent.set()
|
||||
|
||||
def setProgress(self, progress):
|
||||
self._progress = progress
|
||||
self._changeEvent.set()
|
||||
|
||||
def _work(self):
|
||||
while True:
|
||||
self._changeEvent.wait()
|
||||
|
||||
now = time.time()
|
||||
delta = now - self._lastUpdate
|
||||
additionalWaitTime = self._ratelimit - delta
|
||||
if additionalWaitTime > 0:
|
||||
time.sleep(additionalWaitTime)
|
||||
|
||||
data = self.getCurrentData()
|
||||
self._updateCallback(data)
|
||||
self._lastUpdate = time.time()
|
||||
self._changeEvent.clear()
|
||||
|
||||
def getCurrentData(self):
|
||||
return {
|
||||
"state": self._state,
|
||||
"job": self._jobData,
|
||||
"currentZ": self._currentZ,
|
||||
"progress": self._progress
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,391 +0,0 @@
|
|||
# coding=utf-8
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
|
||||
import sys
|
||||
import os
|
||||
import yaml
|
||||
import logging
|
||||
import re
|
||||
import uuid
|
||||
|
||||
APPNAME="OctoPrint"
|
||||
|
||||
instance = None
|
||||
|
||||
def settings(init=False, configfile=None, basedir=None):
|
||||
global instance
|
||||
if instance is None:
|
||||
if init:
|
||||
instance = Settings(configfile, basedir)
|
||||
else:
|
||||
raise ValueError("Settings not initialized yet")
|
||||
return instance
|
||||
|
||||
default_settings = {
|
||||
"serial": {
|
||||
"port": None,
|
||||
"baudrate": None,
|
||||
"autoconnect": False,
|
||||
"log": False,
|
||||
"timeout": {
|
||||
"detection": 0.5,
|
||||
"connection": 2,
|
||||
"communication": 5
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"host": "0.0.0.0",
|
||||
"port": 5000
|
||||
},
|
||||
"webcam": {
|
||||
"stream": None,
|
||||
"snapshot": None,
|
||||
"ffmpeg": None,
|
||||
"bitrate": "5000k",
|
||||
"watermark": True,
|
||||
"flipH": False,
|
||||
"flipV": False
|
||||
},
|
||||
"feature": {
|
||||
"gCodeVisualizer": True,
|
||||
"waitForStartOnConnect": False,
|
||||
"alwaysSendChecksum": False,
|
||||
"sdSupport": True
|
||||
},
|
||||
"folder": {
|
||||
"uploads": None,
|
||||
"timelapse": None,
|
||||
"timelapse_tmp": None,
|
||||
"logs": None,
|
||||
"virtualSd": None
|
||||
},
|
||||
"temperature": {
|
||||
"profiles":
|
||||
[
|
||||
{"name": "ABS", "extruder" : 210, "bed" : 100 },
|
||||
{"name": "PLA", "extruder" : 180, "bed" : 60 }
|
||||
]
|
||||
},
|
||||
"printerParameters": {
|
||||
"movementSpeed": {
|
||||
"x": 6000,
|
||||
"y": 6000,
|
||||
"z": 200,
|
||||
"e": 300
|
||||
},
|
||||
"pauseTriggers": []
|
||||
},
|
||||
"appearance": {
|
||||
"name": "",
|
||||
"color": "default"
|
||||
},
|
||||
"controls": [],
|
||||
"system": {
|
||||
"actions": []
|
||||
},
|
||||
"accessControl": {
|
||||
"enabled": False,
|
||||
"userManager": "octoprint.users.FilebasedUserManager",
|
||||
"userfile": None
|
||||
},
|
||||
<<<<<<< HEAD
|
||||
"curaEngine": {
|
||||
"enabled": False,
|
||||
"path": "/default/path/to/curaEngine/dir/",
|
||||
"config": "/default/path/to/your/curaEngine/config/file/"
|
||||
}
|
||||
=======
|
||||
"events": {
|
||||
"systemCommandTrigger": {
|
||||
"enabled": False
|
||||
},
|
||||
"gcodeCommandTrigger": {
|
||||
"enabled": False
|
||||
}
|
||||
},
|
||||
"api": {
|
||||
"enabled": False,
|
||||
"key": ''.join('%02X' % ord(z) for z in uuid.uuid4().bytes)
|
||||
}
|
||||
>>>>>>> devel
|
||||
}
|
||||
|
||||
valid_boolean_trues = ["true", "yes", "y", "1"]
|
||||
|
||||
class Settings(object):
|
||||
|
||||
def __init__(self, configfile=None, basedir=None):
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
self.settings_dir = None
|
||||
|
||||
self._config = None
|
||||
self._dirty = False
|
||||
|
||||
self._init_settings_dir(basedir)
|
||||
|
||||
if configfile is not None:
|
||||
self._configfile = configfile
|
||||
else:
|
||||
self._configfile = os.path.join(self.settings_dir, "config.yaml")
|
||||
self.load()
|
||||
|
||||
def _init_settings_dir(self, basedir):
|
||||
if basedir is not None:
|
||||
self.settings_dir = basedir
|
||||
else:
|
||||
self.settings_dir = _resolveSettingsDir(APPNAME)
|
||||
|
||||
def _getDefaultFolder(self, type):
|
||||
folder = default_settings["folder"][type]
|
||||
if folder is None:
|
||||
folder = os.path.join(self.settings_dir, type.replace("_", os.path.sep))
|
||||
return folder
|
||||
|
||||
#~~ load and save
|
||||
|
||||
def load(self):
|
||||
if os.path.exists(self._configfile) and os.path.isfile(self._configfile):
|
||||
with open(self._configfile, "r") as f:
|
||||
self._config = yaml.safe_load(f)
|
||||
# chamged from else to handle cases where the file exists, but is empty / 0 bytes
|
||||
if not self._config:
|
||||
self._config = {}
|
||||
|
||||
def save(self, force=False):
|
||||
if not self._dirty and not force:
|
||||
return
|
||||
|
||||
with open(self._configfile, "wb") as configFile:
|
||||
yaml.safe_dump(self._config, configFile, default_flow_style=False, indent=" ", allow_unicode=True)
|
||||
self._dirty = False
|
||||
self.load()
|
||||
|
||||
#~~ getter
|
||||
|
||||
def get(self, path):
|
||||
if len(path) == 0:
|
||||
return None
|
||||
|
||||
config = self._config
|
||||
defaults = default_settings
|
||||
|
||||
while len(path) > 1:
|
||||
key = path.pop(0)
|
||||
if key in config.keys() and key in defaults.keys():
|
||||
config = config[key]
|
||||
defaults = defaults[key]
|
||||
elif key in defaults.keys():
|
||||
config = {}
|
||||
defaults = defaults[key]
|
||||
else:
|
||||
return None
|
||||
|
||||
k = path.pop(0)
|
||||
if not isinstance(k, (list, tuple)):
|
||||
keys = [k]
|
||||
else:
|
||||
keys = k
|
||||
|
||||
results = []
|
||||
for key in keys:
|
||||
if key in config.keys():
|
||||
results.append(config[key])
|
||||
elif key in defaults:
|
||||
results.append(defaults[key])
|
||||
else:
|
||||
results.append(None)
|
||||
|
||||
if not isinstance(k, (list, tuple)):
|
||||
return results.pop()
|
||||
else:
|
||||
return results
|
||||
|
||||
def getInt(self, path):
|
||||
value = self.get(path)
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
self._logger.warn("Could not convert %r to a valid integer when getting option %r" % (value, path))
|
||||
return None
|
||||
|
||||
def getFloat(self, path):
|
||||
value = self.get(path)
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
self._logger.warn("Could not convert %r to a valid integer when getting option %r" % (value, path))
|
||||
return None
|
||||
|
||||
def getBoolean(self, path):
|
||||
value = self.get(path)
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
return value.lower() in valid_boolean_trues
|
||||
|
||||
def getBaseFolder(self, type):
|
||||
if type not in default_settings["folder"].keys():
|
||||
return None
|
||||
|
||||
folder = self.get(["folder", type])
|
||||
if folder is None:
|
||||
folder = self._getDefaultFolder(type)
|
||||
|
||||
if not os.path.isdir(folder):
|
||||
os.makedirs(folder)
|
||||
|
||||
return folder
|
||||
|
||||
def getFeedbackControls(self):
|
||||
feedbackControls = []
|
||||
for control in self.get(["controls"]):
|
||||
feedbackControls.extend(self._getFeedbackControls(control))
|
||||
return feedbackControls
|
||||
|
||||
def _getFeedbackControls(self, control=None):
|
||||
if control["type"] == "feedback_command":
|
||||
pattern = control["regex"]
|
||||
try:
|
||||
matcher = re.compile(pattern)
|
||||
return [(control["name"], matcher, control["template"])]
|
||||
except:
|
||||
# invalid regex or something like this, we'll just skip this entry
|
||||
pass
|
||||
elif control["type"] == "section":
|
||||
result = []
|
||||
for c in control["children"]:
|
||||
result.extend(self._getFeedbackControls(c))
|
||||
return result
|
||||
else:
|
||||
return []
|
||||
|
||||
def getPauseTriggers(self):
|
||||
triggers = {
|
||||
"enable": [],
|
||||
"disable": [],
|
||||
"toggle": []
|
||||
}
|
||||
for trigger in self.get(["printerParameters", "pauseTriggers"]):
|
||||
try:
|
||||
regex = trigger["regex"]
|
||||
type = trigger["type"]
|
||||
if type in triggers.keys():
|
||||
# make sure regex is valid
|
||||
re.compile(regex)
|
||||
# add to type list
|
||||
triggers[type].append(regex)
|
||||
except:
|
||||
# invalid regex or something like this, we'll just skip this entry
|
||||
pass
|
||||
|
||||
result = {}
|
||||
for type in triggers.keys():
|
||||
if len(triggers[type]) > 0:
|
||||
result[type] = re.compile("|".join(map(lambda x: "(%s)" % x, triggers[type])))
|
||||
return result
|
||||
|
||||
#~~ setter
|
||||
|
||||
def set(self, path, value, force=False):
|
||||
if len(path) == 0:
|
||||
return
|
||||
|
||||
config = self._config
|
||||
defaults = default_settings
|
||||
|
||||
while len(path) > 1:
|
||||
key = path.pop(0)
|
||||
if key in config.keys() and key in defaults.keys():
|
||||
config = config[key]
|
||||
defaults = defaults[key]
|
||||
elif key in defaults.keys():
|
||||
config[key] = {}
|
||||
config = config[key]
|
||||
defaults = defaults[key]
|
||||
else:
|
||||
return
|
||||
|
||||
key = path.pop(0)
|
||||
if not force and key in defaults.keys() and key in config.keys() and defaults[key] == value:
|
||||
del config[key]
|
||||
self._dirty = True
|
||||
elif force or (not key in config.keys() and defaults[key] != value) or (key in config.keys() and config[key] != value):
|
||||
if value is None:
|
||||
del config[key]
|
||||
else:
|
||||
config[key] = value
|
||||
self._dirty = True
|
||||
|
||||
def setInt(self, path, value, force=False):
|
||||
if value is None:
|
||||
self.set(path, None, force)
|
||||
return
|
||||
|
||||
try:
|
||||
intValue = int(value)
|
||||
except ValueError:
|
||||
self._logger.warn("Could not convert %r to a valid integer when setting option %r" % (value, path))
|
||||
return
|
||||
|
||||
self.set(path, intValue, force)
|
||||
|
||||
def setFloat(self, path, value, force=False):
|
||||
if value is None:
|
||||
self.set(path, None, force)
|
||||
return
|
||||
|
||||
try:
|
||||
floatValue = float(value)
|
||||
except ValueError:
|
||||
self._logger.warn("Could not convert %r to a valid integer when setting option %r" % (value, path))
|
||||
return
|
||||
|
||||
self.set(path, floatValue, force)
|
||||
|
||||
def setBoolean(self, path, value, force=False):
|
||||
if value is None or isinstance(value, bool):
|
||||
self.set(path, value, force)
|
||||
elif value.lower() in valid_boolean_trues:
|
||||
self.set(path, True, force)
|
||||
else:
|
||||
self.set(path, False, force)
|
||||
|
||||
def setBaseFolder(self, type, path, force=False):
|
||||
if type not in default_settings["folder"].keys():
|
||||
return None
|
||||
|
||||
currentPath = self.getBaseFolder(type)
|
||||
defaultPath = self._getDefaultFolder(type)
|
||||
if (path is None or path == defaultPath) and "folder" in self._config.keys() and type in self._config["folder"].keys():
|
||||
del self._config["folder"][type]
|
||||
if not self._config["folder"]:
|
||||
del self._config["folder"]
|
||||
self._dirty = True
|
||||
elif (path != currentPath and path != defaultPath) or force:
|
||||
if not "folder" in self._config.keys():
|
||||
self._config["folder"] = {}
|
||||
self._config["folder"][type] = path
|
||||
self._dirty = True
|
||||
|
||||
def _resolveSettingsDir(applicationName):
|
||||
# taken from http://stackoverflow.com/questions/1084697/how-do-i-store-desktop-application-data-in-a-cross-platform-way-for-python
|
||||
if sys.platform == "darwin":
|
||||
from AppKit import NSSearchPathForDirectoriesInDomains
|
||||
# http://developer.apple.com/DOCUMENTATION/Cocoa/Reference/Foundation/Miscellaneous/Foundation_Functions/Reference/reference.html#//apple_ref/c/func/NSSearchPathForDirectoriesInDomains
|
||||
# NSApplicationSupportDirectory = 14
|
||||
# NSUserDomainMask = 1
|
||||
# True for expanding the tilde into a fully qualified path
|
||||
return os.path.join(NSSearchPathForDirectoriesInDomains(14, 1, True)[0], applicationName)
|
||||
elif sys.platform == "win32":
|
||||
return os.path.join(os.environ["APPDATA"], applicationName)
|
||||
else:
|
||||
return os.path.expanduser(os.path.join("~", "." + applicationName.lower()))
|
||||
|
|
@ -1,481 +0,0 @@
|
|||
<div id="settings_dialog" class="modal hide fade container" tabindex="-1" role="dialog" aria-labelledby="settings_dialog_label" aria-hidden="true">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h3 id="settings_dialog_label">OctoPrint Settings</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="tabbable">
|
||||
<ul class="nav nav-list span4" id="settingsTabs">
|
||||
<li class="nav-header">Printer</li>
|
||||
<li class="active"><a href="#settings_serialConnection" data-toggle="tab">Serial Connection</a></li>
|
||||
<li><a href="#settings_printerParameters" data-toggle="tab">Printer Parameters</a></li>
|
||||
<li><a href="#settings_temperature" data-toggle="tab">Temperatures</a></li>
|
||||
<li class="nav-header">Features</li>
|
||||
<li><a href="#settings_features" data-toggle="tab">Features</a></li>
|
||||
<li><a href="#settings_webcam" data-toggle="tab">Webcam</a></li>
|
||||
{% if enableAccessControl %}<li><a href="#settings_users" data-toggle="tab">Access Control</a></li>{% endif %}
|
||||
<li><a href="#settings_api" data-toggle="tab">Api</a></li>
|
||||
<li class="nav-header">OctoPrint</li>
|
||||
<li><a href="#settings_folder" data-toggle="tab">Folders</a></li>
|
||||
<li><a href="#settings_appearance" data-toggle="tab">Appearance</a></li>
|
||||
<<<<<<< HEAD
|
||||
<li><a href="#settings_cura" data-toggle="tab">Cura</a></li>
|
||||
{% if enableAccessControl %}<li><a href="#settings_users" data-toggle="tab">Users</a></li>{% endif %}
|
||||
</ul>
|
||||
|
||||
<div class="tab-content span8">
|
||||
<div class="tab-pane active" id="settings_printerParameters">
|
||||
=======
|
||||
</ul>
|
||||
|
||||
<div class="tab-content span8">
|
||||
<div class="tab-pane active" id="settings_serialConnection">
|
||||
<form class="form-horizontal">
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-serialPort">Serial Port</label>
|
||||
<div class="controls">
|
||||
<select id="settings-serialPort" data-bind="options: serial_portOptions, optionsCaption: 'AUTO', value: serial_port"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-baudrate">Baudrate</label>
|
||||
<div class="controls">
|
||||
<select id="settings-baudrate" data-bind="options: serial_baudrateOptions, optionsCaption: 'AUTO', value: serial_baudrate"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" data-bind="checked: serial_autoconnect" id="settings-serialAutoconnect"> Auto-connect to printer on server start
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-movementSpeedE">Communication timeout</label>
|
||||
<div class="controls">
|
||||
<div class="input-append">
|
||||
<input type="number" step="any" min="0" class="input-mini text-right" data-bind="value: serial_timeoutCommunication" id="settings-serialTimeoutCommunication">
|
||||
<span class="add-on">s</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-movementSpeedE">Connection timeout</label>
|
||||
<div class="controls">
|
||||
<div class="input-append">
|
||||
<input type="number" step="any" min="0" class="input-mini text-right" data-bind="value: serial_timeoutConnection" id="settings-serialTimeoutConnection">
|
||||
<span class="add-on">s</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-movementSpeedE">Autodetection timeout</label>
|
||||
<div class="controls">
|
||||
<div class="input-append">
|
||||
<input type="number" step="any" min="0" class="input-mini text-right" data-bind="value: serial_timeoutDetection" id="settings-serialTimeoutDetection">
|
||||
<span class="add-on">s</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" data-bind="checked: serial_log" id="settings-serialLog"> Log communication to serial.log (might negatively impact performance) <span class="label label-important">Warning</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="tab-pane" id="settings_printerParameters">
|
||||
>>>>>>> devel
|
||||
<form class="form-horizontal">
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-movementSpeedX">Movement Speed X Axis</label>
|
||||
<div class="controls">
|
||||
<div class="input-append">
|
||||
<input type="number" class="input-mini text-right" data-bind="value: printer_movementSpeedX" id="settings-movementSpeedX">
|
||||
<span class="add-on">mm/min</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-movementSpeedY">Movement Speed Y Axis</label>
|
||||
<div class="controls">
|
||||
<div class="input-append">
|
||||
<input type="number" class="input-mini text-right" data-bind="value: printer_movementSpeedY" id="settings-movementSpeedY">
|
||||
<span class="add-on">mm/min</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-movementSpeedZ">Movement Speed Z Axis</label>
|
||||
<div class="controls">
|
||||
<div class="input-append">
|
||||
<input type="number" class="input-mini text-right" data-bind="value: printer_movementSpeedZ" id="settings-movementSpeedZ">
|
||||
<span class="add-on">mm/min</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-movementSpeedE">Movement Speed Extruder</label>
|
||||
<div class="controls">
|
||||
<div class="input-append">
|
||||
<input type="number" class="input-mini text-right" data-bind="value: printer_movementSpeedE" id="settings-movementSpeedE">
|
||||
<span class="add-on">mm/min</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="tab-pane" id="settings_webcam">
|
||||
<form class="form-horizontal">
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-webcamStreamUrl">Stream URL</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-block-level" data-bind="value: webcam_streamUrl" id="settings-webcamStreamUrl">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-webcamStreamUrl">Snapshot URL</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-block-level" data-bind="value: webcam_snapshotUrl" id="settings-webcamSnapshotUrl">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-webcamStreamUrl">Path to FFMPEG</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-block-level" data-bind="value: webcam_ffmpegPath" id="settings-webcamFfmpegPath">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-webcamBitrate">Timelapse bitrate</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-block-level" data-bind="value: webcam_bitrate" id="settings-webcamBitrate">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" data-bind="checked: webcam_watermark" id="settings-webcamWatermark"> Enable OctoPrint watermark in timelapse movies
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" data-bind="checked: webcam_flipH" id="settings-webcamFlipH"> Flip webcam horizontally
|
||||
</label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" data-bind="checked: webcam_flipV" id="settings-webcamFlipV"> Flip webcam vertically
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="tab-pane" id="settings_features">
|
||||
<form class="form-horizontal">
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" data-bind="checked: feature_gcodeViewer" id="settings-featureGcodeViewer"> Enable GCode Visualizer
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" data-bind="checked: feature_sdSupport" id="settings-featureSdSupport"> Enable SD support
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" data-bind="checked: feature_waitForStart" id="settings-featureWaitForStart"> Wait for <code>start</code> on connect
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" data-bind="checked: feature_alwaysSendChecksum" id="settings-featureAlwaysSendChecksum"> Send a checksum with <strong>every</strong> command <span class="label">Repetier</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="tab-pane" id="settings_folder">
|
||||
<form class="form-horizontal">
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-folderUploads">Upload Folder</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-block-level" data-bind="value: folder_uploads" id="settings-folderUploads">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-folderTimelapse">Timelapse Folder</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-block-level" data-bind="value: folder_timelapse" id="settings-folderTimelapse">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-folderTimelapseTemp">Timelapse Temp Folder</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-block-level" data-bind="value: folder_timelapseTmp" id="settings-folderTimelapseTemp">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-folderLogs">Logs Folder</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-block-level" data-bind="value: folder_logs" id="settings-folderLogs">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="tab-pane" id="settings_temperature">
|
||||
<form class="form-horizontal">
|
||||
<div class="row-fluid">
|
||||
<div class="offset3 span3"><h4>Extruder</h4></div>
|
||||
<div class="span3"><h4>Bed</h4></div>
|
||||
</div>
|
||||
<div data-bind="foreach: temperature_profiles">
|
||||
<div class="row-fluid" style="margin-bottom: 5px">
|
||||
<div class="span3">
|
||||
<input type="text" class="span12 text-right" data-bind="value: name">
|
||||
</div>
|
||||
<div class="input-append span3">
|
||||
<input type="number" class="input-mini text-right" data-bind="value: extruder">
|
||||
<span class="add-on">°C</span>
|
||||
</div>
|
||||
<div class="input-append span3">
|
||||
<input type="number" class="input-mini text-right" data-bind="value: bed">
|
||||
<span class="add-on">°C</span>
|
||||
</div>
|
||||
<div class="span2">
|
||||
<button title="Remove profile" class="btn btn-danger" data-bind="click: $parent.removeTemperatureProfile"><i class="icon-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-fluid">
|
||||
<div class="offset9 span2">
|
||||
<button title="Add Profile" class="btn btn-primary" data-bind="click: addTemperatureProfile"><i class="icon-plus"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="tab-pane" id="settings_appearance">
|
||||
<form class="form-horizontal">
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-appearanceName">Title</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-block-level" data-bind="value: appearance_name" id="settings-appearanceName">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-appearanceColor">Color</label>
|
||||
<div class="controls">
|
||||
<select id="settings-appearanceColor" data-bind="value: appearance_color, options: appearance_available_colors">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<<<<<<< HEAD
|
||||
<div class="tab-pane" id="settings_cura">
|
||||
<form class="form-horizontal">
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-curaEnabled">Cura Enabled</label>
|
||||
<div class="controls">
|
||||
<input type="checkbox" data-bind="checked: cura_enabled">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-curaPath">CuraEngine Path</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-block-level" data-bind="value: cura_path">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-curaConfig">Cura Config</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-block-level" data-bind="value: cura_config">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
=======
|
||||
<div class="tab-pane" id="settings_api">
|
||||
<form class="form-horizontal">
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="settings-apiEnabled" data-bind="checked: api_enabled"> Enable
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-apiKey">Apikey</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-block-level" data-bind="value: api_key" id="settings-apikey">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
>>>>>>> devel
|
||||
{% if enableAccessControl %}
|
||||
<div class="tab-pane" id="settings_users">
|
||||
<table class="table table-condensed table-hover" id="system_users">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="settings_users_name">Name</th>
|
||||
<th class="settings_users_active">Active</th>
|
||||
<th class="settings_users_admin">Admin</th>
|
||||
<th class="settings_users_actions">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-bind="foreach: users.listHelper.paginatedItems">
|
||||
<tr>
|
||||
<td class="settings_users_name" data-bind="text: name"></td>
|
||||
<td class="settings_users_active"><i data-bind="css: { 'icon-check': active, 'icon-check-empty': !active }"></i></td>
|
||||
<td class="settings_users_admin"><i data-bind="css: { 'icon-check': admin, 'icon-check-empty': !admin }"></i></td>
|
||||
<td class="settings_users_actions" class="system_users_action">
|
||||
<a href="#" class="icon-pencil" title="Update User" data-bind="click: function() { $root.users.showEditUserDialog($data); }"></a> | <a href="#" class="icon-key" title="Change password" data-bind="click: function() { $root.users.showChangePasswordDialog($data); }"></a> | <a href="#" class="icon-trash" title="Delete user" data-bind="click: function() { $root.users.removeUser($data); }"></a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pagination pagination-mini pagination-centered">
|
||||
<ul>
|
||||
<li data-bind="css: {disabled: users.listHelper.currentPage() === 0}"><a href="#" data-bind="click: users.listHelper.prevPage">«</a></li>
|
||||
</ul>
|
||||
<ul data-bind="foreach: users.listHelper.pages">
|
||||
<li data-bind="css: { active: $data.number === $root.users.listHelper.currentPage(), disabled: $data.number === -1 }"><a href="#" data-bind="text: $data.text, click: function() { $root.users.listHelper.changePage($data.number); }"></a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li data-bind="css: {disabled: users.listHelper.currentPage() === users.listHelper.lastPage()}"><a href="#" data-bind="click: users.listHelper.nextPage">»</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button title="Add user" class="btn" data-bind="click: $root.users.showAddUserDialog"><i class="icon-plus"></i> Create new user</button>
|
||||
|
||||
<!-- Modals for user management -->
|
||||
|
||||
<div id="settings-usersDialogAddUser" class="modal hide fade">
|
||||
<div class="modal-header">
|
||||
<a href="#" class="close" data-dismiss="modal" aria-hidden="true">×</a>
|
||||
<h3>Create new user</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-horizontal">
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-usersDialogAddUserName">Username</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-block-level" id="settings-usersDialogAddUserName" data-bind="value: $root.users.editorUsername" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-usersDialogAddUserPassword1">Password</label>
|
||||
<div class="controls">
|
||||
<input type="password" class="input-block-level" id="settings-usersDialogAddUserPassword1" data-bind="value: $root.users.editorPassword" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group" data-bind="css: {error: $root.users.editorPasswordMismatch()}">
|
||||
<label class="control-label" for="settings-usersDialogAddUserPassword2">Repeat Password</label>
|
||||
<div class="controls">
|
||||
<input type="password" class="input-block-level" id="settings-usersDialogAddUserPassword2" data-bind="value: $root.users.editorRepeatedPassword, valueUpdate: 'afterkeydown'" required>
|
||||
<span class="help-inline" data-bind="visible: $root.users.editorPasswordMismatch()">Passwords do not match</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="settings-usersDialogAddUserActive" data-bind="checked: $root.users.editorActive"> Active
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="settings-usersDialogAddUserAdmin" data-bind="checked: $root.users.editorAdmin"> Admin
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn" data-dismiss="modal" aria-hidden="true">Abort</button>
|
||||
<button class="btn btn-primary" data-bind="click: function() { $root.users.confirmAddUser(); }, enable: !$root.users.editorPasswordMismatch()">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="settings-usersDialogEditUser" class="modal hide fade">
|
||||
<div class="modal-header">
|
||||
<a href="#" class="close" data-dismiss="modal" aria-hidden="true">×</a>
|
||||
<h3>Edit user "<span data-bind="text: $root.users.editorUsername"></span>"</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-horizontal">
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="settings-usersDialogEditUserActive" data-bind="checked: $root.users.editorActive"> Active
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="settings-usersDialogEditUserAdmin" data-bind="checked: $root.users.editorAdmin"> Admin
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn" data-dismiss="modal" aria-hidden="true">Abort</button>
|
||||
<button class="btn btn-primary" data-bind="click: function() { $root.users.confirmEditUser(); }">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="settings-usersDialogChangePassword" class="modal hide fade">
|
||||
<div class="modal-header">
|
||||
<a href="#" class="close" data-dismiss="modal" aria-hidden="true">×</a>
|
||||
<h3>Change password for user "<span data-bind="text: $root.users.editorUsername"></span>"</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-horizontal">
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-usersDialogChangePasswordPassword1">New Password</label>
|
||||
<div class="controls">
|
||||
<input type="password" class="input-block-level" id="settings-usersDialogChangePasswordPassword1" data-bind="value: $root.users.editorPassword" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group" data-bind="css: {error: $root.users.editorPasswordMismatch()}">
|
||||
<label class="control-label" for="settings-usersDialogChangePasswordPassword2">Repeat Password</label>
|
||||
<div class="controls">
|
||||
<input type="password" class="input-block-level" id="settings-usersDialogChangePasswordPassword2" data-bind="value: $root.users.editorRepeatedPassword, valueUpdate: 'afterkeydown'" required>
|
||||
<span class="help-inline" data-bind="visible: $root.users.editorPasswordMismatch()">Passwords do not match</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn" data-dismiss="modal" aria-hidden="true">Abort</button>
|
||||
<button class="btn btn-primary" data-bind="click: function() { $root.users.confirmChangePassword(); }, enable: !$root.users.editorPasswordMismatch()">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn" data-dismiss="modal" aria-hidden="true">Cancel</button>
|
||||
<button class="btn btn-primary" data-bind="click: saveData">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
Loading…
Reference in a new issue