diff --git a/.gitignore b/.gitignore
index 65f3bfa..c810f33 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,24 +4,9 @@
*.pyc
*.zip
*.exe
-darwin-Cura-*
-win32-Cura-*
-linux-Cura-*
-Printrun
+*.iml
.idea
.DS_Store
-Cura/current_profile.ini
-Cura/preferences.ini
-cura.sh
-pypy
-python
-printrun.bat
-cura.bat
-object-mirror.png
-object.png
-*darwin.dmg
-scripts/darwin/dist/*
-scripts/darwin/build/*
-scripts/darwin/Cura.dmg.sparseimage
-scripts/win32/dist/*
-log.txt
+build
+dist
+OctoPrint.egg-info
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..34aec81
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,2 @@
+recursive-include octoprint/static *
+recursive-include octoprint/templates *
diff --git a/README.md b/README.md
index 7dd80ab..48c96cd 100644
--- a/README.md
+++ b/README.md
@@ -3,26 +3,23 @@ OctoPrint
[![Flattr this git repo](http://api.flattr.com/button/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=foosel&url=https://github.com/foosel/OctoPrint&title=OctoPrint&language=&tags=github&category=software)
-OctoPrint provides a responsive web interface for controlling a 3D printer (RepRap, Ultimaker, ...). It currently
-allows
+OctoPrint provides a responsive web interface for controlling a 3D printer (RepRap, Ultimaker, ...). It is Free Software
+and released under the [GNU Affero General Public License V3](http://www.gnu.org/licenses/agpl.html).
-* 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)
-* 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
-* 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
+Its website can be found at [octoprint.org](http://octoprint.org).
-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
-monitoring and timelapse support, you'll need a **powered** USB hub.
+Reporting bugs
+--------------
-OctoPrint is Free Software and released under the [GNU Affero General Public License V3](http://www.gnu.org/licenses/agpl.html).
+OctoPrint's issue tracker can be found [on Github](https://github.com/foosel/OctoPrint/issues). **Before opening a new
+ticket please take a look at [this guide on how to file a bug report with OctoPrint](https://github.com/foosel/OctoPrint/wiki/How-to-file-a-bug-report).**
+
+Sending pull requests
+---------------------
+
+Please create all pull requests against the [devel branch](https://github.com/foosel/OctoPrint/tree/devel) of OctoPrint, as that one is used for developing new
+features and then merged against master when those features are deemed mature enough for general consumption. In case
+of bug fixes I'll take care to cherry pick them against master if the bugs they are fixing are critical.
Dependencies
------------
@@ -54,7 +51,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`.
@@ -68,46 +65,14 @@ See `run --help` for further information.
Configuration
-------------
-If not specified via the commandline, the configfile `config.yaml` for OctoPrint is expected in the settings folder, which is located at ~/.octoprint on Linux, at %APPDATA%/OctoPrint on Windows and at ~/Library/Application Support/OctoPrint on MacOS.
+If not specified via the commandline, the configfile `config.yaml` for OctoPrint is expected in the settings folder,
+which is located at `~/.octoprint` on Linux, at `%APPDATA%/OctoPrint` on Windows and
+at `~/Library/Application Support/OctoPrint` on MacOS.
-A comprehensive overview of all available configuration settings can be found [on the wiki](https://github.com/foosel/OctoPrint/wiki/Configuration).
+A comprehensive overview of all available configuration settings can be found
+[on the wiki](https://github.com/foosel/OctoPrint/wiki/Configuration).
Setup on a Raspberry Pi running Raspbian
----------------------------------------
A comprehensive setup guide can be found [on the wiki](https://github.com/foosel/OctoPrint/wiki/Setup-on-a-Raspberry-Pi-running-Raspbian).
-
-Credits
--------
-
-OctoPrint started out as a fork of Cura (https://github.com/daid/Cura) for adding a web interface to its
-printing functionality and was originally named "Printer WebUI". It still uses Cura's communication code for talking to
-the printer, but has been reorganized to only include those parts of Cura necessary for its targeted use case.
-
-It also uses the following libraries and frameworks for backend and frontend:
-
-* Flask: http://flask.pocoo.org/
-* Tornado: http://www.tornadoweb.org/
-* Tornadio2: https://github.com/MrJoes/tornadio2
-* PyYAML: http://pyyaml.org/
-* Socket.io: http://socket.io/
-* jQuery: http://jquery.com/
-* Bootstrap: http://twitter.github.com/bootstrap/
-* Font Awesome: http://fortawesome.github.com/Font-Awesome/
-* Knockout.js: http://knockoutjs.com/
-* Underscore.js: http://underscorejs.org/
-* Flot: http://www.flotcharts.org/
-* jQuery File Upload: http://blueimp.github.com/jQuery-File-Upload/
-* Pines Notify: http://pinesframework.org/pnotify/
-* gCodeVisualizer: https://github.com/hudbrog/gCodeViewer
-
-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).
diff --git a/octoprint/events.py b/octoprint/events.py
new file mode 100644
index 0000000..2a3f313
--- /dev/null
+++ b/octoprint/events.py
@@ -0,0 +1,246 @@
+# coding=utf-8
+
+__author__ = "Lars Norpchen"
+__author__ = "Gina Häußge "
+__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
+
+import datetime
+import logging
+import subprocess
+
+from octoprint.settings import settings
+
+# singleton
+_instance = None
+
+def eventManager():
+ global _instance
+ if _instance is None:
+ _instance = EventManager()
+ return _instance
+
+class EventManager(object):
+ """
+ Handles receiving events and dispatching them to subscribers
+ """
+
+ def __init__(self):
+ self._registeredListeners = {}
+ self._logger = logging.getLogger(__name__)
+
+ def fire(self, event, payload=None):
+ """
+ Fire an event to anyone subscribed to it
+
+ Any object can generate an event and any object can subscribe to the event's name as a string (arbitrary, but
+ case sensitive) and any extra payload data that may pertain to the event.
+
+ Callbacks must implement the signature "callback(event, payload)", with "event" being the event's name and
+ payload being a payload object specific to the event.
+ """
+
+ if not event in self._registeredListeners.keys():
+ return
+ self._logger.debug("Firing event: %s (Payload: %r)" % (event, payload))
+
+ eventListeners = self._registeredListeners[event]
+ for listener in eventListeners:
+ self._logger.debug("Sending action to %r" % listener)
+ try:
+ listener(event, payload)
+ except:
+ self._logger.exception("Got an exception while sending event %s (Payload: %r) to %s" % (event, payload, listener))
+
+
+ def subscribe(self, event, callback):
+ """
+ Subscribe a listener to an event -- pass in the event name (as a string) and the callback object
+ """
+
+ if not event in self._registeredListeners.keys():
+ self._registeredListeners[event] = []
+
+ if callback in self._registeredListeners[event]:
+ # callback is already subscribed to the event
+ return
+
+ self._registeredListeners[event].append(callback)
+ self._logger.debug("Subscribed listener %r for event %s" % (callback, event))
+
+ def unsubscribe (self, event, callback):
+ if not event in self._registeredListeners:
+ # no callback registered for callback, just return
+ return
+
+ if not callback in self._registeredListeners[event]:
+ # callback not subscribed to event, just return
+ return
+
+ self._registeredListeners[event].remove(callback)
+ self._logger.debug("Unsubscribed listener %r for event %s" % (callback, event))
+
+class GenericEventListener(object):
+ """
+ The GenericEventListener can be subclassed to easily create custom event listeners.
+ """
+
+ def __init__(self):
+ self._logger = logging.getLogger(__name__)
+
+ def subscribe(self, events):
+ """
+ Subscribes the eventCallback method for all events in the given list.
+ """
+
+ for event in events:
+ eventManager().subscribe(event, self.eventCallback)
+
+ def unsubscribe(self, events):
+ """
+ Unsubscribes the eventCallback method for all events in the given list
+ """
+
+ for event in events:
+ eventManager().unsubscribe(event, self.eventCallback)
+
+ def eventCallback(self, event, payload):
+ """
+ Actual event callback called with name of event and optional payload. Not implemented here, override in
+ child classes.
+ """
+ pass
+
+class DebugEventListener(GenericEventListener):
+ def __init__(self):
+ GenericEventListener.__init__(self)
+
+ events = ["Startup", "Connected", "Disconnected", "ClientOpen", "ClientClosed", "PowerOn", "PowerOff", "Upload",
+ "FileSelected", "TransferStarted", "TransferDone", "PrintStarted", "PrintDone", "PrintFailed",
+ "Cancelled", "Home", "ZChange", "Paused", "Waiting", "Cooling", "Alert", "Conveyor", "Eject",
+ "CaptureStart", "CaptureDone", "MovieDone", "EStop", "Error"]
+ self.subscribe(events)
+
+ def eventCallback(self, event, payload):
+ GenericEventListener.eventCallback(self, event, payload)
+ self._logger.debug("Received event: %s (Payload: %r)" % (event, payload))
+
+class CommandTrigger(GenericEventListener):
+ def __init__(self, triggerType, printer):
+ GenericEventListener.__init__(self)
+ self._printer = printer
+ self._subscriptions = {}
+
+ self._initSubscriptions(triggerType)
+
+ def _initSubscriptions(self, triggerType):
+ """
+ Subscribes all events as defined in "events > $triggerType > subscriptions" in the settings with their
+ respective commands.
+ """
+ if not settings().get(["events", triggerType]):
+ return
+
+ if not settings().getBoolean(["events", triggerType, "enabled"]):
+ return
+
+ eventsToSubscribe = []
+ for subscription in settings().get(["events", triggerType, "subscriptions"]):
+ if not "event" in subscription.keys() or not "command" in subscription.keys():
+ self._logger.info("Invalid %s, missing either event or command: %r" % (triggerType, subscription))
+ continue
+
+ event = subscription["event"]
+ command = subscription["command"]
+
+ if not event in self._subscriptions.keys():
+ self._subscriptions[event] = []
+ self._subscriptions[event].append(command)
+
+ if not event in eventsToSubscribe:
+ eventsToSubscribe.append(event)
+
+ self.subscribe(eventsToSubscribe)
+
+ def eventCallback(self, event, payload):
+ """
+ Event callback, iterates over all subscribed commands for the given event, processes the command
+ string and then executes the command via the abstract executeCommand method.
+ """
+
+ GenericEventListener.eventCallback(self, event, payload)
+
+ if not event in self._subscriptions:
+ return
+
+ for command in self._subscriptions[event]:
+ processedCommand = self._processCommand(command, payload)
+ self.executeCommand(processedCommand)
+
+ def executeCommand(self, command):
+ """
+ Not implemented, override in child classes
+ """
+ pass
+
+ def _processCommand(self, command, payload):
+ """
+ Performs string substitutions in the command string based on a couple of current parameters.
+
+ The following substitutions are currently supported:
+
+ - %(currentZ)s : current Z position of the print head, or -1 if not available
+ - %(filename)s : current selected filename, or "NO FILE" if no file is selected
+ - %(progress)s : current print progress in percent, 0 if no print is in progress
+ - %(data)s : the string representation of the event's payload
+ - %(now)s : ISO 8601 representation of the current date and time
+ """
+
+ params = {
+ "currentZ": "-1",
+ "filename": "NO FILE",
+ "progress": "0",
+ "data": str(payload),
+ "now": datetime.datetime.now().isoformat()
+ }
+
+ currentData = self._printer.getCurrentData()
+
+ if "currentZ" in currentData.keys() and currentData["currentZ"] is not None:
+ params["currentZ"] = str(currentData["currentZ"])
+
+ if "job" in currentData.keys() and currentData["job"] is not None:
+ params["filename"] = currentData["job"]["filename"]
+ if "progress" in currentData.keys() and currentData["progress"] is not None \
+ and "progress" in currentData["progress"].keys() and currentData["progress"]["progress"] is not None:
+ params["progress"] = str(round(currentData["progress"]["progress"] * 100))
+
+ return command % params
+
+class SystemCommandTrigger(CommandTrigger):
+ """
+ Performs configured system commands for configured events.
+ """
+
+ def __init__(self, printer):
+ CommandTrigger.__init__(self, "systemCommandTrigger", printer)
+
+ def executeCommand(self, command):
+ try:
+ self._logger.info("Executing system command: %s" % command)
+ subprocess.Popen(command, shell=True)
+ except subprocess.CalledProcessError, e:
+ self._logger.warn("Command failed with return code %i: %s" % (e.returncode, e.message))
+ except Exception, ex:
+ self._logger.exception("Command failed")
+
+class GcodeCommandTrigger(CommandTrigger):
+ """
+ Sends configured GCODE commands to the printer for configured events.
+ """
+
+ def __init__(self, printer):
+ CommandTrigger.__init__(self, "gcodeCommandTrigger", printer)
+
+ def executeCommand(self, command):
+ self._logger.debug("Executing GCode command: %s" % command)
+ self._printer.commands(command.split(","))
diff --git a/octoprint/gcodefiles.py b/octoprint/gcodefiles.py
index f4cfb41..3b37121 100644
--- a/octoprint/gcodefiles.py
+++ b/octoprint/gcodefiles.py
@@ -15,6 +15,8 @@ from octoprint.settings import settings
from werkzeug.utils import secure_filename
+SUPPORTED_EXTENSIONS=["gcode", "gco"]
+
class GcodeManager:
def __init__(self):
self._logger = logging.getLogger(__name__)
@@ -63,6 +65,8 @@ class GcodeManager:
dirty = True
if gcode.extrusionAmount:
analysisResult["filament"] = "%.2fm" % (gcode.extrusionAmount / 1000)
+ if gcode.calculateVolumeCm3():
+ analysisResult["filament"] += " / %.2fcm³" % gcode.calculateVolumeCm3()
dirty = True
if dirty:
@@ -114,28 +118,44 @@ class GcodeManager:
#~~ file handling
def addFile(self, file):
- if file:
- absolutePath = self.getAbsolutePath(file.filename, mustExist=False)
- if absolutePath is not None:
- if file.filename in self._metadata.keys():
- # delete existing metadata entry, since the file is going to get overwritten
- del self._metadata[file.filename]
- self._metadataDirty = True
- self._saveMetadata()
- file.save(absolutePath)
- self._metadataAnalyzer.addFileToQueue(os.path.basename(absolutePath))
- return self._getBasicFilename(absolutePath)
- return None
+ if not file:
+ return None
+
+ absolutePath = self.getAbsolutePath(file.filename, mustExist=False)
+ 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)
def removeFile(self, filename):
filename = self._getBasicFilename(filename)
absolutePath = self.getAbsolutePath(filename)
- if absolutePath is not None:
- os.remove(absolutePath)
- if filename in self._metadata.keys():
- del self._metadata[filename]
- self._metadataDirty = True
- self._saveMetadata()
+ 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):
"""
@@ -153,7 +173,7 @@ class GcodeManager:
"""
filename = self._getBasicFilename(filename)
- if not util.isAllowedFile(filename, set(["gcode"])):
+ if not util.isAllowedFile(filename.lower(), set(SUPPORTED_EXTENSIONS)):
return None
secure = os.path.join(self._uploadFolder, secure_filename(self._getBasicFilename(filename)))
diff --git a/octoprint/printer.py b/octoprint/printer.py
index be080b2..9429bbc 100644
--- a/octoprint/printer.py
+++ b/octoprint/printer.py
@@ -8,10 +8,13 @@ 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():
"""
@@ -21,12 +24,16 @@ def getConnectionOptions():
"ports": comm.serialList(),
"baudrates": comm.baudrateList(),
"portPreference": settings().get(["serial", "port"]),
- "baudratePreference": settings().getInt(["serial", "baudrate"])
+ "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
@@ -34,19 +41,19 @@ class Printer():
self._targetTemp = None
self._targetBedTemp = None
self._temps = {
- "actual": [],
- "target": [],
- "actualBed": [],
- "targetBed": []
+ "actual": deque([], 300),
+ "target": deque([], 300),
+ "actualBed": deque([], 300),
+ "targetBed": deque([], 300)
}
self._tempBacklog = []
self._latestMessage = None
- self._messages = []
+ self._messages = deque([], 300)
self._messageBacklog = []
self._latestLog = None
- self._log = []
+ self._log = deque([], 300)
self._logBacklog = []
self._state = None
@@ -57,16 +64,14 @@ class Printer():
self._printTime = None
self._printTimeLeft = None
- # gcode handling
- self._gcodeList = None
- self._filename = None
- self._gcodeLoader = None
+ self._printAfterSelect = False
- # feedrate
- self._feedrateModifierMapping = {"outerWall": "WALL-OUTER", "innerWall": "WALL_INNER", "fill": "FILL", "support": "SUPPORT"}
+ # sd handling
+ self._sdPrinting = False
+ self._sdStreaming = False
+ self._sdFilelistAvailable = threading.Event()
- # timelapse
- self._timelapse = None
+ self._selectedFile = None
# comm
self._comm = None
@@ -84,9 +89,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},
- 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
)
@@ -120,7 +124,25 @@ class Printer():
try: callback.sendCurrentData(copy.deepcopy(data))
except: pass
-#~~ printer commands
+ def _sendTriggerUpdateCallbacks(self, type):
+ for callback in self._callbacks:
+ try: callback.sendUpdateTrigger(type)
+ except: pass
+
+ 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):
"""
@@ -138,6 +160,7 @@ class Printer():
if self._comm is not None:
self._comm.close()
self._comm = None
+ eventManager().fire("Disconnected")
def command(self, command):
"""
@@ -149,48 +172,48 @@ class Printer():
"""
Sends multiple gcode commands (provided as a list) to the printer.
"""
+ if self._comm is None:
+ return
+
for command in commands:
self._comm.sendCommand(command)
- def setFeedrateModifier(self, structure, percentage):
- if (not self._feedrateModifierMapping.has_key(structure)) or percentage < 0:
+ def setTemperatureOffset(self, extruder, bed):
+ if self._comm is None:
return
- self._comm.setFeedrateModifier(self._feedrateModifierMapping[structure], percentage / 100.0)
+ self._comm.setTemperatureOffset(extruder, bed)
+ self._stateMonitor.setTempOffsets(extruder, bed)
- 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):
+ def selectFile(self, filename, sd, printAfterSelect=False):
+ if self._comm is None or (self._comm.isBusy() or self._comm.isStreaming()):
return
- self._setJobData(None, None)
+ self._printAfterSelect = printAfterSelect
+ self._comm.selectFile(filename, sd)
+ self._setProgressData(0, None, None, None)
+ self._setCurrentZ(None)
- onGcodeLoadedCallback = self._onGcodeLoaded
- if printAfterLoading:
- onGcodeLoadedCallback = self._onGcodeLoadedToPrint
+ def unselectFile(self):
+ if self._comm is not None and (self._comm.isBusy() or self._comm.isStreaming()):
+ return
- self._gcodeLoader = GcodeLoader(file, self._onGcodeLoadingProgress, onGcodeLoadedCallback)
- self._gcodeLoader.start()
+ self._comm.unselectFile()
+ self._setProgressData(0, None, None, None)
+ self._setCurrentZ(None)
- 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:
- return
- if self._comm.isPrinting():
+ if self._selectedFile is None:
return
- self._setCurrentZ(-1)
- self._comm.printGCode(self._gcodeList)
+ self._setCurrentZ(None)
+ self._comm.startPrint()
def togglePausePrint(self):
"""
@@ -198,6 +221,7 @@ class Printer():
"""
if self._comm is None:
return
+
self._comm.setPause(not self._comm.isPaused())
def cancelPrint(self, disableMotorsAndHeater=True):
@@ -206,28 +230,23 @@ class Printer():
"""
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 line, height, print time
+ # reset progress, height, print time
self._setCurrentZ(None)
- self._setProgressData(None, None, None)
+ self._setProgressData(None, None, None, None)
# mark print as failure
- self._gcodeManager.printFailed(self._filename)
+ if self._selectedFile is not None:
+ self._gcodeManager.printFailed(self._selectedFile["filename"])
+ eventManager().fire("PrintFailed", self._selectedFile["filename"])
#~~ state monitoring
- def setTimelapse(self, timelapse):
- if self._timelapse is not None and self.isPrinting():
- self._timelapse.onPrintjobStopped()
- del self._timelapse
- self._timelapse = timelapse
-
- def getTimelapse(self):
- return self._timelapse
-
def _setCurrentZ(self, currentZ):
self._currentZ = currentZ
@@ -242,15 +261,13 @@ class Printer():
def _addLog(self, log):
self._log.append(log)
- self._log = self._log[-300:]
self._stateMonitor.addLog(log)
def _addMessage(self, message):
self._messages.append(message)
- self._messages = self._messages[-300:]
self._stateMonitor.addMessage(message)
- def _setProgressData(self, progress, printTime, printTimeLeft):
+ def _setProgressData(self, progress, filepos, printTime, printTimeLeft):
self._progress = progress
self._printTime = printTime
self._printTimeLeft = printTimeLeft
@@ -263,22 +280,19 @@ class Printer():
if (self._printTimeLeft):
formattedPrintTimeLeft = util.getFormattedTimeDelta(datetime.timedelta(minutes=self._printTimeLeft))
- self._stateMonitor.setProgress({"progress": self._progress, "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)
self._temps["actual"].append((currentTimeUtc, temp))
- self._temps["actual"] = self._temps["actual"][-300:]
-
self._temps["target"].append((currentTimeUtc, targetTemp))
- self._temps["target"] = self._temps["target"][-300:]
-
self._temps["actualBed"].append((currentTimeUtc, bedTemp))
- self._temps["actualBed"] = self._temps["actualBed"][-300:]
-
self._temps["targetBed"].append((currentTimeUtc, bedTargetTemp))
- self._temps["targetBed"] = self._temps["targetBed"][-300:]
self._temp = temp
self._bedTemp = bedTemp
@@ -287,19 +301,31 @@ 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
+ fileMTime = None
filament = None
- if self._filename:
- formattedFilename = os.path.basename(self._filename)
+ 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():
@@ -308,15 +334,17 @@ 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, "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": self._temps,
- "logHistory": self._log,
- "messageHistory": self._messages
+ "temperatureHistory": temps,
+ "logHistory": list(self._log),
+ "messageHistory": list(self._messages)
})
callback.sendHistoryData(data)
except Exception, err:
@@ -325,16 +353,24 @@ class Printer():
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(),
- "loading": self.isLoading(),
"paused": self.isPaused(),
- "ready": self.isReady()
+ "ready": self.isReady(),
+ "sdReady": sdReady
}
+ def getCurrentData(self):
+ return self._stateMonitor.getCurrentData()
+
#~~ callbacks triggered from self._comm
def mcLog(self, message):
@@ -352,26 +388,19 @@ class Printer():
"""
oldState = self._state
- # forward relevant state changes to timelapse
- if self._timelapse is not None:
- 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)
-
# 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)
- self._gcodeManager.resumeAnalysis() # do not analyse gcode while 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() # printing done, put those cpu cycles to good use
+ self._gcodeManager.pauseAnalysis() # do not analyse gcode while printing
self._setState(state)
-
def mcMessage(self, message):
"""
Callback method for the comm object, called upon message exchanges via serial.
@@ -379,66 +408,109 @@ class Printer():
"""
self._addMessage(message)
- def mcProgress(self, lineNr):
+ def mcProgress(self):
"""
Callback method for the comm object, called upon any change in progress of the printjob.
- Triggers storage of new values for printTime, printTimeLeft and the current line.
+ Triggers storage of new values for printTime, printTimeLeft and the current progress.
"""
- oldProgress = self._progress
- if self._timelapse is not None:
- try: self._timelapse.onPrintjobProgress(oldProgress, self._progress, int(round(self._progress * 100 / len(self._gcodeList))))
- except: pass
-
- self._setProgressData(self._comm.getPrintPos(), self._comm.getPrintTime(), self._comm.getPrintTimeRemainingEstimate())
+ 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 self._timelapse is not None:
- self._timelapse.onZChange(oldZ, newZ)
+ 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)
- #~~ 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)
- self._gcodeLoader = None
-
- self._stateMonitor.setGcodeData({"filename": None, "progress": None})
+ def mcSdStateChange(self, sdReady):
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
- def _onGcodeLoadedToPrint(self, filename, gcodeList):
- self._onGcodeLoaded(filename, gcodeList)
- self.startPrint()
+ def mcSdFiles(self, files):
+ self._sendTriggerUpdateCallbacks("gcodeFiles")
+ self._sdFilelistAvailable.set()
+
+ 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 or not self._comm.isSdReady():
+ return []
+ return self._comm.getSdFiles()
+
+ def addSdFile(self, filename, path):
+ if not self._comm or self._comm.isBusy() or not self._comm.isSdReady():
+ return
+
+ self.refreshSdFiles(blocking=True)
+ existingSdFiles = self._comm.getSdFiles()
+
+ sdFilename = util.getDosFilename(filename, existingSdFiles)
+ self._comm.startFileTransfer(path, sdFilename)
+
+ def deleteSdFile(self, filename):
+ if not self._comm or not self._comm.isSdReady():
+ return
+ self._comm.deleteSdFile(filename)
+
+ def initSdCard(self):
+ if not self._comm or self._comm.isSdReady():
+ return
+ self._comm.initSdCard()
+
+ def releaseSdCard(self):
+ if not self._comm or not self._comm.isSdReady():
+ return
+ self._comm.releaseSdCard()
+
+ def refreshSdFiles(self, blocking=False):
+ """
+ Refreshs the list of file stored on the SD card attached to printer (if available and printer communication
+ available). Optional blocking parameter allows making the method block (max 10s) until the file list has been
+ received (and can be accessed via self._comm.getSdFiles()). Defaults to a asynchronous operation.
+ """
+ if not self._comm or not self._comm.isSdReady():
+ return
+ self._sdFilelistAvailable.clear()
+ self._comm.refreshSdFiles()
+ if blocking:
+ self._sdFilelistAvailable.wait(10000)
#~~ 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.
@@ -448,6 +520,33 @@ class Printer():
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):
+ if self._comm is not None:
+ (tempOffset, bedTempOffset) = self._comm.getOffsets()
+ else:
+ tempOffset = 0
+ bedTempOffset = 0
+
+ return {
+ "extruder": {
+ "current": self._temp,
+ "target": self._targetTemp,
+ "offset": tempOffset
+ },
+ "bed": {
+ "current": self._bedTemp,
+ "target": self._targetBedTemp,
+ "offset": bedTempOffset
+ }
+ }
+
def isClosedOrError(self):
return self._comm is None or self._comm.isClosedOrError()
@@ -464,7 +563,7 @@ class Printer():
return self._comm is not None and self._comm.isError()
def isReady(self):
- return self._gcodeLoader is None and self._gcodeList and len(self._gcodeList) > 0
+ return self.isOperational() and not self._comm.isStreaming()
def isLoading(self):
return self._gcodeLoader is not None
@@ -488,7 +587,7 @@ class GcodeLoader(threading.Thread):
def run(self):
#Send an initial M110 to reset the line counter to zero.
prevLineType = lineType = "CUSTOM"
- gcodeList = ["M110"]
+ gcodeList = ["M110 N0"]
filesize = os.stat(self._filename).st_size
with open(self._filename, "r") as file:
for line in file:
@@ -514,6 +613,38 @@ class GcodeLoader(threading.Thread):
def _onParsingProgress(self, progress):
self._progressCallback(self._filename, progress, "parsing")
+class SdFileStreamer(threading.Thread):
+ def __init__(self, comm, filename, file, progressCallback, finishCallback):
+ threading.Thread.__init__(self)
+
+ self._comm = comm
+ self._filename = filename
+ self._file = file
+ self._progressCallback = progressCallback
+ self._finishCallback = finishCallback
+
+ def run(self):
+ if self._comm.isBusy():
+ return
+
+ name = self._filename[:self._filename.rfind(".")]
+ sdFilename = name[:8].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
@@ -525,9 +656,13 @@ class StateMonitor(object):
self._state = None
self._jobData = None
self._gcodeData = None
+ self._sdUploadData = None
self._currentZ = None
self._progress = None
+ self._tempOffset = 0
+ self._bedTempOffset = 0
+
self._changeEvent = threading.Event()
self._lastUpdate = time.time()
@@ -535,10 +670,9 @@ class StateMonitor(object):
self._worker.daemon = True
self._worker.start()
- def reset(self, state=None, jobData=None, gcodeData=None, progress=None, currentZ=None):
+ def reset(self, state=None, jobData=None, progress=None, currentZ=None):
self.setState(state)
self.setJobData(jobData)
- self.setGcodeData(gcodeData)
self.setProgress(progress)
self.setCurrentZ(currentZ)
@@ -566,14 +700,17 @@ class StateMonitor(object):
self._jobData = jobData
self._changeEvent.set()
- def setGcodeData(self, gcodeData):
- self._gcodeData = gcodeData
- self._changeEvent.set()
-
def setProgress(self, progress):
self._progress = progress
self._changeEvent.set()
+ def setTempOffsets(self, tempOffset, bedTempOffset):
+ if tempOffset is not None:
+ self._tempOffset = tempOffset
+ if bedTempOffset is not None:
+ self._bedTempOffset = bedTempOffset
+ self._changeEvent.set()
+
def _work(self):
while True:
self._changeEvent.wait()
@@ -593,8 +730,8 @@ class StateMonitor(object):
return {
"state": self._state,
"job": self._jobData,
- "gcode": self._gcodeData,
"currentZ": self._currentZ,
- "progress": self._progress
+ "progress": self._progress,
+ "offsets": (self._tempOffset, self._bedTempOffset)
}
diff --git a/octoprint/server.py b/octoprint/server.py
index 1f3831c..d9566ae 100644
--- a/octoprint/server.py
+++ b/octoprint/server.py
@@ -2,33 +2,44 @@
__author__ = "Gina Häußge "
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
-from werkzeug.utils import secure_filename
-import tornadio2
-from flask import Flask, request, render_template, jsonify, send_from_directory, url_for, current_app, session, abort
+from werkzeug.utils import secure_filename, redirect
+from sockjs.tornado import SockJSRouter, SockJSConnection
+from flask import Flask, request, render_template, jsonify, send_from_directory, url_for, current_app, session, abort, make_response
from flask.ext.login import LoginManager, login_user, logout_user, login_required, current_user
from flask.ext.principal import Principal, Permission, RoleNeed, Identity, identity_changed, AnonymousIdentity, identity_loaded, UserNeed
+from functools import wraps
+
import os
import threading
import logging, logging.config
import subprocess
+import netaddr
from octoprint.printer import Printer, getConnectionOptions
from octoprint.settings import settings, valid_boolean_trues
-import octoprint.timelapse as timelapse
+import octoprint.timelapse
import octoprint.gcodefiles as gcodefiles
import octoprint.util as util
import octoprint.users as users
+import octoprint.events as events
+
SUCCESS = {}
BASEURL = "/ajax/"
+APIBASEURL = "/api/"
app = Flask("octoprint")
# Only instantiated by the Server().run() method
# In order that threads don't start too early when running as a Daemon
-printer = None
+printer = None
+timelapse = None
+debug = False
+
gcodeManager = None
userManager = None
+eventManager = None
+loginManager = None
principals = Principal(app)
admin_permission = Permission(RoleNeed("admin"))
@@ -36,9 +47,9 @@ user_permission = Permission(RoleNeed("user"))
#~~ Printer state
-class PrinterStateConnection(tornadio2.SocketConnection):
- def __init__(self, printer, gcodeManager, userManager, session, endpoint=None):
- tornadio2.SocketConnection.__init__(self, session, endpoint)
+class PrinterStateConnection(SockJSConnection):
+ def __init__(self, printer, gcodeManager, userManager, eventManager, session):
+ SockJSConnection.__init__(self, session)
self._logger = logging.getLogger(__name__)
@@ -52,18 +63,34 @@ class PrinterStateConnection(tornadio2.SocketConnection):
self._printer = printer
self._gcodeManager = gcodeManager
self._userManager = userManager
+ self._eventManager = eventManager
+
+ def _getRemoteAddress(self, info):
+ forwardedFor = info.headers.get("X-Forwarded-For")
+ if forwardedFor is not None:
+ return forwardedFor.split(",")[0]
+ return info.ip
def on_open(self, info):
- self._logger.info("New connection from client")
- # Use of global here is smelly
- printer.registerCallback(self)
- gcodeManager.registerCallback(self)
+ self._logger.info("New connection from client: %s" % self._getRemoteAddress(info))
+ self._printer.registerCallback(self)
+ self._gcodeManager.registerCallback(self)
+ octoprint.timelapse.registerCallback(self)
+
+ self._eventManager.fire("ClientOpened")
+ self._eventManager.subscribe("MovieDone", self._onMovieDone)
+
+ global timelapse
+ octoprint.timelapse.notifyCallbacks(timelapse)
def on_close(self):
self._logger.info("Closed client connection")
- # Use of global here is smelly
- printer.unregisterCallback(self)
- gcodeManager.unregisterCallback(self)
+ self._printer.unregisterCallback(self)
+ self._gcodeManager.unregisterCallback(self)
+ octoprint.timelapse.unregisterCallback(self)
+
+ self._eventManager.fire("ClientClosed")
+ self._eventManager.unsubscribe("MovieDone", self._onMovieDone)
def on_message(self, message):
pass
@@ -87,13 +114,19 @@ class PrinterStateConnection(tornadio2.SocketConnection):
"logs": logs,
"messages": messages
})
- self.emit("current", data)
+ self._emit("current", data)
def sendHistoryData(self, data):
- self.emit("history", data)
+ self._emit("history", data)
def sendUpdateTrigger(self, type):
- self.emit("updateTrigger", type)
+ self._emit("updateTrigger", type)
+
+ def sendFeedbackCommandOutput(self, name, output):
+ self._emit("feedbackCommandOutput", {"name": name, "output": output})
+
+ def sendTimelapseConfig(self, timelapseConfig):
+ self._emit("timelapse", timelapseConfig)
def addLog(self, data):
with self._logBacklogMutex:
@@ -107,20 +140,65 @@ class PrinterStateConnection(tornadio2.SocketConnection):
with self._temperatureBacklogMutex:
self._temperatureBacklog.append(data)
+ def _onMovieDone(self, event, payload):
+ self.sendUpdateTrigger("timelapseFiles")
+
+ def _emit(self, type, payload):
+ self.send({type: payload})
+
+def restricted_access(func):
+ """
+ If you decorate a view with this, it will ensure that first setup has been
+ done for OctoPrint's Access Control plus that any conditions of the
+ login_required decorator are met.
+
+ If OctoPrint's Access Control has not been setup yet (indicated by the "firstRun"
+ flag from the settings being set to True and the userManager not indicating
+ that it's user database has been customized from default), the decorator
+ will cause a HTTP 403 status code to be returned by the decorated resource.
+
+ Otherwise the result of calling login_required will be returned.
+ """
+ @wraps(func)
+ def decorated_view(*args, **kwargs):
+ if settings().getBoolean(["server", "firstRun"]) and (userManager is None or not userManager.hasBeenCustomized()):
+ return make_response("OctoPrint isn't setup yet", 403)
+ return login_required(func)(*args, **kwargs)
+ return decorated_view
+
# Did attempt to make webserver an encapsulated class but ended up with __call__ failures
@app.route("/")
def index():
+ branch = None
+ commit = None
+ try:
+ branch, commit = util.getGitInfo()
+ except:
+ pass
+
+ global debug
+
return render_template(
"index.jinja2",
ajaxBaseUrl=BASEURL,
webcamStream=settings().get(["webcam", "stream"]),
enableTimelapse=(settings().get(["webcam", "snapshot"]) is not None and settings().get(["webcam", "ffmpeg"]) is not None),
enableGCodeVisualizer=settings().get(["feature", "gCodeVisualizer"]),
+ enableTemperatureGraph=settings().get(["feature", "temperatureGraph"]),
enableSystemMenu=settings().get(["system"]) is not None and settings().get(["system", "actions"]) is not None and len(settings().get(["system", "actions"])) > 0,
- enableAccessControl=userManager is not None
+ enableAccessControl=userManager is not None,
+ enableSdSupport=settings().get(["feature", "sdSupport"]),
+ firstRun=settings().getBoolean(["server", "firstRun"]) and (userManager is None or not userManager.hasBeenCustomized()),
+ debug=debug,
+ gitBranch=branch,
+ gitCommit=commit
)
+@app.route("/robots.txt")
+def robotsTxt():
+ return send_from_directory(app.static_folder, "robots.txt")
+
#~~ Printer control
@app.route(BASEURL + "control/connection/options", methods=["GET"])
@@ -128,7 +206,7 @@ def connectionOptions():
return jsonify(getConnectionOptions())
@app.route(BASEURL + "control/connection", methods=["POST"])
-@login_required
+@restricted_access
def connect():
if "command" in request.values.keys() and request.values["command"] == "connect":
port = None
@@ -141,6 +219,9 @@ def connect():
settings().set(["serial", "port"], port)
settings().setInt(["serial", "baudrate"], baudrate)
settings().save()
+ if "autoconnect" in request.values.keys():
+ settings().setBoolean(["serial", "autoconnect"], True)
+ settings().save()
printer.connect(port=port, baudrate=baudrate)
elif "command" in request.values.keys() and request.values["command"] == "disconnect":
printer.disconnect()
@@ -148,7 +229,7 @@ def connect():
return jsonify(SUCCESS)
@app.route(BASEURL + "control/command", methods=["POST"])
-@login_required
+@restricted_access
def printerCommand():
if "application/json" in request.headers["Content-Type"]:
data = request.json
@@ -172,7 +253,7 @@ def printerCommand():
return jsonify(SUCCESS)
@app.route(BASEURL + "control/job", methods=["POST"])
-@login_required
+@restricted_access
def printJobControl():
if "command" in request.values.keys():
if request.values["command"] == "start":
@@ -184,7 +265,7 @@ def printJobControl():
return jsonify(SUCCESS)
@app.route(BASEURL + "control/temperature", methods=["POST"])
-@login_required
+@restricted_access
def setTargetTemperature():
if "temp" in request.values.keys():
# set target temperature
@@ -196,10 +277,28 @@ def setTargetTemperature():
bedTemp = request.values["bedTemp"]
printer.command("M140 S" + bedTemp)
+ if "tempOffset" in request.values.keys():
+ # set target temperature offset
+ try:
+ tempOffset = float(request.values["tempOffset"])
+ if tempOffset >= -50 and tempOffset <= 50:
+ printer.setTemperatureOffset(tempOffset, None)
+ except:
+ pass
+
+ if "bedTempOffset" in request.values.keys():
+ # set target bed temperature offset
+ try:
+ bedTempOffset = float(request.values["bedTempOffset"])
+ if bedTempOffset >= -50 and bedTempOffset <= 50:
+ printer.setTemperatureOffset(None, bedTempOffset)
+ except:
+ pass
+
return jsonify(SUCCESS)
@app.route(BASEURL + "control/jog", methods=["POST"])
-@login_required
+@restricted_access
def jog():
if not printer.isOperational() or printer.isPrinting():
# do not jog when a print job is running or we don't have a connection
@@ -231,100 +330,221 @@ 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"])
return jsonify(controls=customControls)
+@app.route(BASEURL + "control/sd", methods=["POST"])
+@restricted_access
+def sdCommand():
+ if not settings().getBoolean(["feature", "sdSupport"]) or not printer.isOperational() or printer.isPrinting():
+ return jsonify(SUCCESS)
+
+ if "command" in request.values.keys():
+ command = request.values["command"]
+ if command == "init":
+ printer.initSdCard()
+ elif command == "refresh":
+ printer.refreshSdFiles()
+ elif command == "release":
+ printer.releaseSdCard()
+
+ return jsonify(SUCCESS)
+
#~~ GCODE file handling
@app.route(BASEURL + "gcodefiles", methods=["GET"])
def readGcodeFiles():
- return jsonify(files=gcodeManager.getAllFileData())
+ files = gcodeManager.getAllFileData()
+
+ sdFileList = printer.getSdFiles()
+ if sdFileList is not None:
+ for sdFile in sdFileList:
+ files.append({
+ "name": sdFile,
+ "size": "n/a",
+ "bytes": 0,
+ "date": "n/a",
+ "origin": "sd"
+ })
+ return jsonify(files=files, free=util.getFormattedSize(util.getFreeBytes(settings().getBaseFolder("uploads"))))
@app.route(BASEURL + "gcodefiles/", methods=["GET"])
def readGcodeFile(filename):
- return send_from_directory(settings().getBaseFolder("uploads"), filename, as_attachment=True)
+ return redirectToTornado(request, "/downloads/gcode/" + filename)
@app.route(BASEURL + "gcodefiles/upload", methods=["POST"])
-@login_required
+@restricted_access
def uploadGcodeFile():
- filename = None
if "gcode_file" in request.files.keys():
file = request.files["gcode_file"]
+ sd = "target" in request.values.keys() and request.values["target"] == "sd";
+
+ currentFilename = None
+ currentSd = None
+ currentJob = printer.getCurrentJob()
+ if currentJob is not None and "filename" in currentJob.keys() and "sd" in currentJob.keys():
+ currentFilename = currentJob["filename"]
+ currentSd = currentJob["sd"]
+
+ futureFilename = gcodeManager.getFutureFilename(file)
+ if futureFilename is None:
+ return make_response("Can not upload file %s, wrong format?" % file.filename, 400)
+
+ if futureFilename == currentFilename and sd == currentSd and printer.isPrinting() or printer.isPaused():
+ # trying to overwrite currently selected file, but it is being printed
+ return make_response("Trying to overwrite file that is currently being printed: %s" % currentFilename, 403)
+
filename = gcodeManager.addFile(file)
+ if filename is None:
+ return make_response("Could not upload the file %s" % file.filename, 500)
+
+ absFilename = gcodeManager.getAbsolutePath(filename)
+ if sd:
+ printer.addSdFile(filename, absFilename)
+
+ if currentFilename == filename and currentSd == sd:
+ # reload file as it was updated
+ if sd:
+ printer.selectFile(filename, sd, False)
+ else:
+ printer.selectFile(absFilename, sd, False)
+
+ global eventManager
+ eventManager.fire("Upload", filename)
return jsonify(files=gcodeManager.getAllFileData(), filename=filename)
+
@app.route(BASEURL + "gcodefiles/load", methods=["POST"])
-@login_required
+@restricted_access
def loadGcodeFile():
if "filename" in request.values.keys():
printAfterLoading = False
if "print" in request.values.keys() and request.values["print"] in valid_boolean_trues:
printAfterLoading = True
- filename = gcodeManager.getAbsolutePath(request.values["filename"])
- if filename is not None:
- printer.loadGcode(filename, printAfterLoading)
+
+ sd = False
+ if "target" in request.values.keys() and request.values["target"] == "sd":
+ filename = request.values["filename"]
+ sd = True
+ else:
+ filename = gcodeManager.getAbsolutePath(request.values["filename"])
+ printer.selectFile(filename, sd, printAfterLoading)
return jsonify(SUCCESS)
@app.route(BASEURL + "gcodefiles/delete", methods=["POST"])
-@login_required
+@restricted_access
def deleteGcodeFile():
if "filename" in request.values.keys():
filename = request.values["filename"]
- gcodeManager.removeFile(filename)
+ sd = "target" in request.values.keys() and request.values["target"] == "sd"
+
+ currentJob = printer.getCurrentJob()
+ currentFilename = None
+ currentSd = None
+ if currentJob is not None and "filename" in currentJob.keys() and "sd" in currentJob.keys():
+ currentFilename = currentJob["filename"]
+ currentSd = currentJob["sd"]
+
+ if currentFilename is not None and filename == currentFilename and not (printer.isPrinting() or printer.isPaused()):
+ printer.unselectFile()
+
+ if not (currentFilename == filename and currentSd == sd and (printer.isPrinting() or printer.isPaused())):
+ if sd:
+ printer.deleteSdFile(filename)
+ else:
+ gcodeManager.removeFile(filename)
return readGcodeFiles()
+@app.route(BASEURL + "gcodefiles/refresh", methods=["POST"])
+@restricted_access
+def refreshFiles():
+ printer.updateSdFiles()
+ return jsonify(SUCCESS)
+
+#-- very simple api routines
+@app.route(APIBASEURL + "load", methods=["POST"])
+def apiLoad():
+ logger = logging.getLogger(__name__)
+
+ if not settings().get(["api", "enabled"]):
+ abort(401)
+
+ if not "apikey" in request.values.keys():
+ abort(401)
+
+ if request.values["apikey"] != settings().get(["api", "key"]):
+ abort(403)
+
+ if not "file" in request.files.keys():
+ abort(400)
+
+ # Perform an upload
+ file = request.files["file"]
+ filename = gcodeManager.addFile(file)
+ if filename is None:
+ logger.warn("Upload via API failed")
+ abort(500)
+
+ # Immediately perform a file select and possibly print too
+ printAfterSelect = False
+ if "print" in request.values.keys() and request.values["print"] in valid_boolean_trues:
+ printAfterSelect = True
+ filepath = gcodeManager.getAbsolutePath(filename)
+ if filepath is not None:
+ printer.selectFile(filepath, False, printAfterSelect)
+ return jsonify(SUCCESS)
+
+@app.route(APIBASEURL + "state", methods=["GET"])
+def apiPrinterState():
+ if not settings().get(["api", "enabled"]):
+ abort(401)
+
+ if not "apikey" in request.values.keys():
+ abort(401)
+
+ if request.values["apikey"] != settings().get(["api", "key"]):
+ abort(403)
+
+ currentData = printer.getCurrentData()
+ currentData.update({
+ "temperatures": printer.getCurrentTemperatures()
+ })
+ return jsonify(currentData)
+
#~~ timelapse handling
@app.route(BASEURL + "timelapse", methods=["GET"])
def getTimelapseData():
- lapse = printer.getTimelapse()
+ global timelapse
type = "off"
additionalConfig = {}
- if lapse is not None and isinstance(lapse, timelapse.ZTimelapse):
+ if timelapse is not None and isinstance(timelapse, octoprint.timelapse.ZTimelapse):
type = "zchange"
- elif lapse is not None and isinstance(lapse, timelapse.TimedTimelapse):
+ elif timelapse is not None and isinstance(timelapse, octoprint.timelapse.TimedTimelapse):
type = "timed"
additionalConfig = {
- "interval": lapse.interval()
+ "interval": timelapse.interval()
}
- files = timelapse.getFinishedTimelapses()
+ files = octoprint.timelapse.getFinishedTimelapses()
for file in files:
- file["url"] = url_for("downloadTimelapse", filename=file["name"])
+ file["url"] = "/downloads/timelapse/" + file["name"]
return jsonify({
- "type": type,
- "config": additionalConfig,
- "files": files
+ "type": type,
+ "config": additionalConfig,
+ "files": files
})
@app.route(BASEURL + "timelapse/", methods=["GET"])
def downloadTimelapse(filename):
- if util.isAllowedFile(filename, set(["mpg"])):
- return send_from_directory(settings().getBaseFolder("timelapse"), filename, as_attachment=True)
+ return redirectToTornado(request, "/downloads/timelapse/" + filename)
@app.route(BASEURL + "timelapse/", methods=["DELETE"])
-@login_required
+@restricted_access
def deleteTimelapse(filename):
if util.isAllowedFile(filename, set(["mpg"])):
secure = os.path.join(settings().getBaseFolder("timelapse"), secure_filename(filename))
@@ -333,25 +553,58 @@ def deleteTimelapse(filename):
return getTimelapseData()
@app.route(BASEURL + "timelapse", methods=["POST"])
-@login_required
+@restricted_access
def setTimelapseConfig():
if request.values.has_key("type"):
- type = request.values["type"]
- lapse = None
- if "zchange" == type:
- lapse = timelapse.ZTimelapse()
- elif "timed" == type:
+ config = {
+ "type": request.values["type"],
+ "options": {}
+ }
+
+ if request.values.has_key("interval"):
interval = 10
- if request.values.has_key("interval"):
- try:
- interval = int(request.values["interval"])
- except ValueError:
- pass
- lapse = timelapse.TimedTimelapse(interval)
- printer.setTimelapse(lapse)
+ try:
+ interval = int(request.values["interval"])
+ except ValueError:
+ pass
+
+ config["options"] = {
+ "interval": interval
+ }
+
+ if admin_permission.can() and request.values.has_key("save") and request.values["save"] in valid_boolean_trues:
+ _configureTimelapse(config, True)
+ else:
+ _configureTimelapse(config)
return getTimelapseData()
+def _configureTimelapse(config=None, persist=False):
+ global timelapse
+
+ if config is None:
+ config = settings().get(["webcam", "timelapse"])
+
+ if timelapse is not None:
+ timelapse.unload()
+
+ type = config["type"]
+ if type is None or "off" == type:
+ timelapse = None
+ elif "zchange" == type:
+ timelapse = octoprint.timelapse.ZTimelapse()
+ elif "timed" == type:
+ interval = 10
+ if "options" in config and "interval" in config["options"]:
+ interval = config["options"]["interval"]
+ timelapse = octoprint.timelapse.TimedTimelapse(interval)
+
+ octoprint.timelapse.notifyCallbacks(timelapse)
+
+ if persist:
+ settings().set(["webcam", "timelapse"], config)
+ settings().save()
+
#~~ settings
@app.route(BASEURL + "settings", methods=["GET"])
@@ -360,7 +613,13 @@ def getSettings():
[movementSpeedX, movementSpeedY, movementSpeedZ, movementSpeedE] = s.get(["printerParameters", "movementSpeed", ["x", "y", "z", "e"]])
+ connectionOptions = getConnectionOptions()
+
return jsonify({
+ "api": {
+ "enabled": s.getBoolean(["api", "enabled"]),
+ "key": s.get(["api", "key"])
+ },
"appearance": {
"name": s.get(["appearance", "name"]),
"color": s.get(["appearance", "color"])
@@ -376,11 +635,28 @@ def getSettings():
"snapshotUrl": s.get(["webcam", "snapshot"]),
"ffmpegPath": s.get(["webcam", "ffmpeg"]),
"bitrate": s.get(["webcam", "bitrate"]),
- "watermark": s.getBoolean(["webcam", "watermark"])
+ "watermark": s.getBoolean(["webcam", "watermark"]),
+ "flipH": s.getBoolean(["webcam", "flipH"]),
+ "flipV": s.getBoolean(["webcam", "flipV"])
},
"feature": {
"gcodeViewer": s.getBoolean(["feature", "gCodeVisualizer"]),
- "waitForStart": s.getBoolean(["feature", "waitForStartOnConnect"])
+ "temperatureGraph": s.getBoolean(["feature", "temperatureGraph"]),
+ "waitForStart": s.getBoolean(["feature", "waitForStartOnConnect"]),
+ "alwaysSendChecksum": s.getBoolean(["feature", "alwaysSendChecksum"]),
+ "sdSupport": s.getBoolean(["feature", "sdSupport"]),
+ "swallowOkAfterResend": s.getBoolean(["feature", "swallowOkAfterResend"])
+ },
+ "serial": {
+ "port": connectionOptions["portPreference"],
+ "baudrate": connectionOptions["baudratePreference"],
+ "portOptions": connectionOptions["ports"],
+ "baudrateOptions": connectionOptions["baudrates"],
+ "autoconnect": s.getBoolean(["serial", "autoconnect"]),
+ "timeoutConnection": s.getFloat(["serial", "timeout", "connection"]),
+ "timeoutDetection": s.getFloat(["serial", "timeout", "detection"]),
+ "timeoutCommunication": s.getFloat(["serial", "timeout", "communication"]),
+ "log": s.getBoolean(["serial", "log"])
},
"folder": {
"uploads": s.getBaseFolder("uploads"),
@@ -392,18 +668,24 @@ def getSettings():
"profiles": s.get(["temperature", "profiles"])
},
"system": {
- "actions": s.get(["system", "actions"])
- }
+ "actions": s.get(["system", "actions"]),
+ "events": s.get(["system", "events"])
+ },
+ "terminalFilters": s.get(["terminalFilters"])
})
@app.route(BASEURL + "settings", methods=["POST"])
-@login_required
+@restricted_access
@admin_permission.require(403)
def setSettings():
if "application/json" in request.headers["Content-Type"]:
data = request.json
s = settings()
+ if "api" in data.keys():
+ if "enabled" in data["api"].keys(): s.set(["api", "enabled"], data["api"]["enabled"])
+ if "key" in data["api"].keys(): s.set(["api", "key"], data["api"]["key"], True)
+
if "appearance" in data.keys():
if "name" in data["appearance"].keys(): s.set(["appearance", "name"], data["appearance"]["name"])
if "color" in data["appearance"].keys(): s.set(["appearance", "color"], data["appearance"]["color"])
@@ -420,10 +702,35 @@ def setSettings():
if "ffmpegPath" in data["webcam"].keys(): s.set(["webcam", "ffmpeg"], data["webcam"]["ffmpegPath"])
if "bitrate" in data["webcam"].keys(): s.set(["webcam", "bitrate"], data["webcam"]["bitrate"])
if "watermark" in data["webcam"].keys(): s.setBoolean(["webcam", "watermark"], data["webcam"]["watermark"])
+ if "flipH" in data["webcam"].keys(): s.setBoolean(["webcam", "flipH"], data["webcam"]["flipH"])
+ if "flipV" in data["webcam"].keys(): s.setBoolean(["webcam", "flipV"], data["webcam"]["flipV"])
if "feature" in data.keys():
if "gcodeViewer" in data["feature"].keys(): s.setBoolean(["feature", "gCodeVisualizer"], data["feature"]["gcodeViewer"])
+ if "temperatureGraph" in data["feature"].keys(): s.setBoolean(["feature", "temperatureGraph"], data["feature"]["temperatureGraph"])
if "waitForStart" in data["feature"].keys(): s.setBoolean(["feature", "waitForStartOnConnect"], data["feature"]["waitForStart"])
+ if "alwaysSendChecksum" in data["feature"].keys(): s.setBoolean(["feature", "alwaysSendChecksum"], data["feature"]["alwaysSendChecksum"])
+ if "sdSupport" in data["feature"].keys(): s.setBoolean(["feature", "sdSupport"], data["feature"]["sdSupport"])
+ if "swallowOkAfterResend" in data["feature"].keys(): s.setBoolean(["feature", "swallowOkAfterResend"], data["feature"]["swallowOkAfterResend"])
+
+ if "serial" in data.keys():
+ if "autoconnect" in data["serial"].keys(): s.setBoolean(["serial", "autoconnect"], data["serial"]["autoconnect"])
+ if "port" in data["serial"].keys(): s.set(["serial", "port"], data["serial"]["port"])
+ if "baudrate" in data["serial"].keys(): s.setInt(["serial", "baudrate"], data["serial"]["baudrate"])
+ if "timeoutConnection" in data["serial"].keys(): s.setFloat(["serial", "timeout", "connection"], data["serial"]["timeoutConnection"])
+ if "timeoutDetection" in data["serial"].keys(): s.setFloat(["serial", "timeout", "detection"], data["serial"]["timeoutDetection"])
+ if "timeoutCommunication" in data["serial"].keys(): s.setFloat(["serial", "timeout", "communication"], data["serial"]["timeoutCommunication"])
+
+ oldLog = s.getBoolean(["serial", "log"])
+ if "log" in data["serial"].keys(): s.setBoolean(["serial", "log"], data["serial"]["log"])
+ if oldLog and not s.getBoolean(["serial", "log"]):
+ # disable debug logging to serial.log
+ logging.getLogger("SERIAL").debug("Disabling serial logging")
+ logging.getLogger("SERIAL").setLevel(logging.CRITICAL)
+ elif not oldLog and s.getBoolean(["serial", "log"]):
+ # enable debug logging to serial.log
+ logging.getLogger("SERIAL").setLevel(logging.DEBUG)
+ logging.getLogger("SERIAL").debug("Enabling serial logging")
if "folder" in data.keys():
if "uploads" in data["folder"].keys(): s.setBaseFolder("uploads", data["folder"]["uploads"])
@@ -434,17 +741,46 @@ def setSettings():
if "temperature" in data.keys():
if "profiles" in data["temperature"].keys(): s.set(["temperature", "profiles"], data["temperature"]["profiles"])
+ if "terminalFilters" in data.keys():
+ s.set(["terminalFilters"], data["terminalFilters"])
+
if "system" in data.keys():
if "actions" in data["system"].keys(): s.set(["system", "actions"], data["system"]["actions"])
-
+ if "events" in data["system"].keys(): s.set(["system", "events"], data["system"]["events"])
s.save()
return getSettings()
+@app.route(BASEURL + "setup", methods=["POST"])
+def firstRunSetup():
+ global userManager
+
+ if not settings().getBoolean(["server", "firstRun"]):
+ abort(403)
+
+ if "ac" in request.values.keys() and request.values["ac"] in valid_boolean_trues and \
+ "user" in request.values.keys() and "pass1" in request.values.keys() and \
+ "pass2" in request.values.keys() and request.values["pass1"] == request.values["pass2"]:
+ # configure access control
+ settings().setBoolean(["accessControl", "enabled"], True)
+ userManager.addUser(request.values["user"], request.values["pass1"], True, ["user", "admin"])
+ settings().setBoolean(["server", "firstRun"], False)
+ elif "ac" in request.values.keys() and not request.values["ac"] in valid_boolean_trues:
+ # disable access control
+ settings().setBoolean(["accessControl", "enabled"], False)
+ settings().setBoolean(["server", "firstRun"], False)
+
+ userManager = None
+ loginManager.anonymous_user = users.DummyUser
+ principals.identity_loaders.appendleft(users.dummy_identity_loader)
+
+ settings().save()
+ return jsonify(SUCCESS)
+
#~~ user settings
@app.route(BASEURL + "users", methods=["GET"])
-@login_required
+@restricted_access
@admin_permission.require(403)
def getUsers():
if userManager is None:
@@ -453,7 +789,7 @@ def getUsers():
return jsonify({"users": userManager.getAllUsers()})
@app.route(BASEURL + "users", methods=["POST"])
-@login_required
+@restricted_access
@admin_permission.require(403)
def addUser():
if userManager is None:
@@ -477,7 +813,7 @@ def addUser():
return getUsers()
@app.route(BASEURL + "users/", methods=["GET"])
-@login_required
+@restricted_access
def getUser(username):
if userManager is None:
return jsonify(SUCCESS)
@@ -492,7 +828,7 @@ def getUser(username):
abort(403)
@app.route(BASEURL + "users/", methods=["PUT"])
-@login_required
+@restricted_access
@admin_permission.require(403)
def updateUser(username):
if userManager is None:
@@ -517,7 +853,7 @@ def updateUser(username):
abort(404)
@app.route(BASEURL + "users/", methods=["DELETE"])
-@login_required
+@restricted_access
@admin_permission.require(http_exception=403)
def removeUser(username):
if userManager is None:
@@ -530,7 +866,7 @@ def removeUser(username):
abort(404)
@app.route(BASEURL + "users//password", methods=["PUT"])
-@login_required
+@restricted_access
def changePasswordForUser(username):
if userManager is None:
return jsonify(SUCCESS)
@@ -550,7 +886,7 @@ def changePasswordForUser(username):
#~~ system control
@app.route(BASEURL + "system", methods=["POST"])
-@login_required
+@restricted_access
@admin_permission.require(403)
def performSystemAction():
logger = logging.getLogger(__name__)
@@ -578,7 +914,7 @@ def login():
username = request.values["user"]
password = request.values["pass"]
- if "remember" in request.values.keys() and request.values["remember"]:
+ if "remember" in request.values.keys() and request.values["remember"] == "true":
remember = True
else:
remember = False
@@ -593,11 +929,32 @@ def login():
elif "passive" in request.values.keys():
user = current_user
if user is not None and not user.is_anonymous():
+ identity_changed.send(current_app._get_current_object(), identity=Identity(user.get_id()))
return jsonify(user.asDict())
+ elif settings().getBoolean(["accessControl", "autologinLocal"]) \
+ and settings().get(["accessControl", "autologinAs"]) is not None \
+ and settings().get(["accessControl", "localNetworks"]) is not None:
+
+ autologinAs = settings().get(["accessControl", "autologinAs"])
+ localNetworks = netaddr.IPSet([])
+ for ip in settings().get(["accessControl", "localNetworks"]):
+ localNetworks.add(ip)
+
+ try:
+ remoteAddr = util.getRemoteAddress(request)
+ if netaddr.IPAddress(remoteAddr) in localNetworks:
+ user = userManager.findUser(autologinAs)
+ if user is not None:
+ login_user(user)
+ identity_changed.send(current_app._get_current_object(), identity=Identity(user.get_id()))
+ return jsonify(user.asDict())
+ except:
+ logger = logging.getLogger(__name__)
+ logger.exception("Could not autologin user %s for networks %r" % (autologinAs, localNetworks))
return jsonify(SUCCESS)
@app.route(BASEURL + "logout", methods=["POST"])
-@login_required
+@restricted_access
def logout():
# Remove session keys set by Flask-Principal
for key in ('identity.id', 'identity.auth_type'):
@@ -625,25 +982,122 @@ def load_user(id):
return userManager.findUser(id)
return users.DummyUser()
+def redirectToTornado(request, target):
+ requestUrl = request.url
+ appBaseUrl = requestUrl[:requestUrl.find(BASEURL)]
+
+ redirectUrl = appBaseUrl + target
+ if "?" in requestUrl:
+ fragment = requestUrl[requestUrl.rfind("?"):]
+ redirectUrl += fragment
+ return redirect(redirectUrl)
+
+#~~ customized large response handler
+
+from tornado.web import StaticFileHandler, HTTPError
+import datetime, stat, mimetypes, email, time
+
+class LargeResponseHandler(StaticFileHandler):
+
+ CHUNK_SIZE = 16 * 1024
+
+ def initialize(self, path, default_filename=None, as_attachment=False):
+ StaticFileHandler.initialize(self, path, default_filename)
+ self._as_attachment = as_attachment
+
+ def get(self, path, include_body=True):
+ path = self.parse_url_path(path)
+ abspath = os.path.abspath(os.path.join(self.root, path))
+ # os.path.abspath strips a trailing /
+ # it needs to be temporarily added back for requests to root/
+ if not (abspath + os.path.sep).startswith(self.root):
+ raise HTTPError(403, "%s is not in root static directory", path)
+ if os.path.isdir(abspath) and self.default_filename is not None:
+ # need to look at the request.path here for when path is empty
+ # but there is some prefix to the path that was already
+ # trimmed by the routing
+ if not self.request.path.endswith("/"):
+ self.redirect(self.request.path + "/")
+ return
+ abspath = os.path.join(abspath, self.default_filename)
+ if not os.path.exists(abspath):
+ raise HTTPError(404)
+ if not os.path.isfile(abspath):
+ raise HTTPError(403, "%s is not a file", path)
+
+ stat_result = os.stat(abspath)
+ modified = datetime.datetime.fromtimestamp(stat_result[stat.ST_MTIME])
+
+ self.set_header("Last-Modified", modified)
+
+ mime_type, encoding = mimetypes.guess_type(abspath)
+ if mime_type:
+ self.set_header("Content-Type", mime_type)
+
+ cache_time = self.get_cache_time(path, modified, mime_type)
+
+ if cache_time > 0:
+ self.set_header("Expires", datetime.datetime.utcnow() +
+ datetime.timedelta(seconds=cache_time))
+ self.set_header("Cache-Control", "max-age=" + str(cache_time))
+
+ self.set_extra_headers(path)
+
+ # Check the If-Modified-Since, and don't send the result if the
+ # content has not been modified
+ ims_value = self.request.headers.get("If-Modified-Since")
+ if ims_value is not None:
+ date_tuple = email.utils.parsedate(ims_value)
+ if_since = datetime.datetime.fromtimestamp(time.mktime(date_tuple))
+ if if_since >= modified:
+ self.set_status(304)
+ return
+
+ if not include_body:
+ assert self.request.method == "HEAD"
+ self.set_header("Content-Length", stat_result[stat.ST_SIZE])
+ else:
+ with open(abspath, "rb") as file:
+ while True:
+ data = file.read(LargeResponseHandler.CHUNK_SIZE)
+ if not data:
+ break
+ self.write(data)
+ self.flush()
+
+ def set_extra_headers(self, path):
+ if self._as_attachment:
+ self.set_header("Content-Disposition", "attachment")
+
#~~ startup code
class Server():
- def __init__(self, configfile=None, basedir=None, host="0.0.0.0", port=5000, debug=False):
+ def __init__(self, configfile=None, basedir=None, host="0.0.0.0", port=5000, debug=False, allowRoot=False):
self._configfile = configfile
self._basedir = basedir
self._host = host
self._port = port
self._debug = debug
+ self._allowRoot = allowRoot
+
def run(self):
+ if not self._allowRoot:
+ self._checkForRoot()
+
# Global as I can't work out a way to get it into PrinterStateConnection
global printer
global gcodeManager
global userManager
-
+ global eventManager
+ global loginManager
+ global debug
+
from tornado.wsgi import WSGIContainer
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
- from tornado.web import Application, FallbackHandler
+ from tornado.web import Application, FallbackHandler, StaticFileHandler
+
+ debug = self._debug
# first initialize the settings singleton and make sure it uses given configfile and basedir if available
self._initSettings(self._configfile, self._basedir)
@@ -652,9 +1106,19 @@ class Server():
self._initLogging(self._debug)
logger = logging.getLogger(__name__)
+ eventManager = events.eventManager()
gcodeManager = gcodefiles.GcodeManager()
printer = Printer(gcodeManager)
+ # configure timelapse
+ _configureTimelapse()
+
+ # setup system and gcode command triggers
+ events.SystemCommandTrigger(printer)
+ events.GcodeCommandTrigger(printer)
+ if self._debug:
+ events.DebugEventListener()
+
if settings().getBoolean(["accessControl", "enabled"]):
userManagerName = settings().get(["accessControl", "userManager"])
try:
@@ -664,13 +1128,13 @@ class Server():
logger.exception("Could not instantiate user manager %s, will run with accessControl disabled!" % userManagerName)
app.secret_key = "k3PuVYgtxNm8DXKKTw2nWmFQQun9qceV"
- login_manager = LoginManager()
- login_manager.session_protection = "strong"
- login_manager.user_callback = load_user
+ loginManager = LoginManager()
+ loginManager.session_protection = "strong"
+ loginManager.user_callback = load_user
if userManager is None:
- login_manager.anonymous_user = users.DummyUser
+ loginManager.anonymous_user = users.DummyUser
principals.identity_loaders.appendleft(users.dummy_identity_loader)
- login_manager.init_app(app)
+ loginManager.init_app(app)
if self._host is None:
self._host = settings().get(["server", "host"])
@@ -680,18 +1144,35 @@ class Server():
logger.info("Listening on http://%s:%d" % (self._host, self._port))
app.debug = self._debug
- self._router = tornadio2.TornadioRouter(self._createSocketConnection)
+ self._router = SockJSRouter(self._createSocketConnection, "/sockjs")
self._tornado_app = Application(self._router.urls + [
- (".*", FallbackHandler, {"fallback": WSGIContainer(app)})
+ (r"/downloads/timelapse/([^/]*\.mpg)", LargeResponseHandler, {"path": settings().getBaseFolder("timelapse"), "as_attachment": True}),
+ (r"/downloads/gcode/([^/]*\.(gco|gcode))", LargeResponseHandler, {"path": settings().getBaseFolder("uploads"), "as_attachment": True}),
+ (r".*", FallbackHandler, {"fallback": WSGIContainer(app)})
])
self._server = HTTPServer(self._tornado_app)
self._server.listen(self._port, address=self._host)
- IOLoop.instance().start()
- def _createSocketConnection(self, session, endpoint=None):
- global printer, gcodeManager, userManager
- return PrinterStateConnection(printer, gcodeManager, userManager, session, endpoint)
+ eventManager.fire("Startup")
+ if settings().getBoolean(["serial", "autoconnect"]):
+ (port, baudrate) = settings().get(["serial", "port"]), settings().getInt(["serial", "baudrate"])
+ connectionOptions = getConnectionOptions()
+ if port in connectionOptions["ports"]:
+ printer.connect(port, baudrate)
+ try:
+ IOLoop.instance().start()
+ except:
+ logger.fatal("Now that is embarrassing... Something really really went wrong here. Please report this including the stacktrace below in OctoPrint's bugtracker. Thanks!")
+ logger.exception("Stacktrace follows:")
+
+ def _createSocketConnection(self, session):
+ global printer, gcodeManager, userManager, eventManager
+ return PrinterStateConnection(printer, gcodeManager, userManager, eventManager, session)
+
+ def _checkForRoot(self):
+ if "geteuid" in dir(os) and os.geteuid() == 0:
+ exit("You should not run OctoPrint as root!")
def _initSettings(self, configfile, basedir):
s = settings(init=True, basedir=basedir, configfile=configfile)
@@ -728,6 +1209,17 @@ class Server():
}
},
"loggers": {
+ #"octoprint.timelapse": {
+ # "level": "DEBUG"
+ #},
+ #"octoprint.events": {
+ # "level": "DEBUG"
+ #},
+ "SERIAL": {
+ "level": "CRITICAL",
+ "handlers": ["serialFile"],
+ "propagate": False
+ }
},
"root": {
"level": "INFO",
@@ -736,14 +1228,15 @@ class Server():
}
if debug:
- config["loggers"]["SERIAL"] = {
- "level": "DEBUG",
- "handlers": ["serialFile"],
- "propagate": False
- }
+ config["root"]["level"] = "DEBUG"
logging.config.dictConfig(config)
+ if settings().getBoolean(["serial", "log"]):
+ # enable debug logging to serial.log
+ logging.getLogger("SERIAL").setLevel(logging.DEBUG)
+ logging.getLogger("SERIAL").debug("Enabling serial logging")
+
if __name__ == "__main__":
octoprint = Server()
octoprint.run()
diff --git a/octoprint/settings.py b/octoprint/settings.py
index 7c385f8..7e0fab7 100644
--- a/octoprint/settings.py
+++ b/octoprint/settings.py
@@ -2,11 +2,12 @@
__author__ = "Gina Häußge "
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
-import ConfigParser
import sys
import os
import yaml
import logging
+import re
+import uuid
APPNAME="OctoPrint"
@@ -24,28 +25,48 @@ def settings(init=False, configfile=None, basedir=None):
default_settings = {
"serial": {
"port": None,
- "baudrate": None
+ "baudrate": None,
+ "autoconnect": False,
+ "log": False,
+ "timeout": {
+ "detection": 0.5,
+ "connection": 2,
+ "communication": 5
+ },
+ "additionalPorts": []
},
"server": {
"host": "0.0.0.0",
- "port": 5000
+ "port": 5000,
+ "firstRun": True
},
"webcam": {
"stream": None,
"snapshot": None,
"ffmpeg": None,
"bitrate": "5000k",
- "watermark": True
+ "watermark": True,
+ "flipH": False,
+ "flipV": False,
+ "timelapse": {
+ "type": "off",
+ "options": {}
+ }
},
"feature": {
"gCodeVisualizer": True,
- "waitForStartOnConnect": False
+ "temperatureGraph": True,
+ "waitForStartOnConnect": False,
+ "alwaysSendChecksum": False,
+ "sdSupport": True,
+ "swallowOkAfterResend": False
},
"folder": {
"uploads": None,
"timelapse": None,
"timelapse_tmp": None,
- "logs": None
+ "logs": None,
+ "virtualSd": None
},
"temperature": {
"profiles":
@@ -60,7 +81,8 @@ default_settings = {
"y": 6000,
"z": 200,
"e": 300
- }
+ },
+ "pauseTriggers": []
},
"appearance": {
"name": "",
@@ -71,9 +93,36 @@ default_settings = {
"actions": []
},
"accessControl": {
- "enabled": False,
+ "enabled": True,
"userManager": "octoprint.users.FilebasedUserManager",
- "userfile": None
+ "userfile": None,
+ "autologinLocal": False,
+ "localNetworks": ["127.0.0.0/8"],
+ "autologinAs": None
+ },
+ "events": {
+ "systemCommandTrigger": {
+ "enabled": False
+ },
+ "gcodeCommandTrigger": {
+ "enabled": False
+ }
+ },
+ "api": {
+ "enabled": False,
+ "key": ''.join('%02X' % ord(z) for z in uuid.uuid4().bytes)
+ },
+ "terminalFilters": [
+ { "name": "Suppress M105 requests/responses", "regex": "(Send: M105)|(Recv: ok T:)" },
+ { "name": "Suppress M27 requests/responses", "regex": "(Send: M27)|(Recv: SD printing byte)" }
+ ],
+ "devel": {
+ "virtualPrinter": {
+ "enabled": False,
+ "okAfterResend": False,
+ "forceChecksum": False,
+ "okWithLinenumber": False
+ }
}
}
@@ -115,7 +164,8 @@ class Settings(object):
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)
- else:
+ # 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):
@@ -178,6 +228,17 @@ class Settings(object):
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:
@@ -199,6 +260,54 @@ class Settings(object):
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" or control["type"] == "feedback":
+ 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):
@@ -234,6 +343,7 @@ class Settings(object):
def setInt(self, path, value, force=False):
if value is None:
self.set(path, None, force)
+ return
try:
intValue = int(value)
@@ -243,6 +353,19 @@ class Settings(object):
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)
diff --git a/octoprint/static/css/octoprint.less b/octoprint/static/css/octoprint.less
index 2f8c258..835e7d7 100644
--- a/octoprint/static/css/octoprint.less
+++ b/octoprint/static/css/octoprint.less
@@ -5,13 +5,48 @@ body {
}
.navbar-inner-text (@base) {
- text-shadow: 0 1px 0 lighten(@base, 15%);
- color: contrast(@base, #333333, #f2f2f2);
+ text-shadow: 0 1px 0 contrast(@base, lighten(@base, 15%), darken(@base, 15%));
+ color: @text-color;
+ @caret-color: average(@base, @text-color);
+ @caret-hover-color: average(@caret-color, @text-color);
+
+ .caret {
+ border-bottom-color: @caret-color;
+ border-top-color: @caret-color;
+ }
+
+ &:hover .caret, &:focus .caret {
+ border-bottom-color: @caret-hover-color;
+ border-top-color: @caret-hover-color;
+ }
+}
+
+.brand (@color, @dark, @light) when (@color = @dark) {
+ span {
+ background-image: url(../img/tentacle-20x20.png);
+
+ @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
+ background-image: url(../img/tentacle-20x20@2x.png);
+ }
+ }
+}
+.brand (@color, @dark, @light) when (@color = @light) {
+ span {
+ background-image: url(../img/tentacle-20x20-light.png);
+
+ @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
+ background-image: url(../img/tentacle-20x20-light@2x.png);
+ }
+ }
}
.navbar-inner-color (@base) {
@top: lighten(@base, 25%);
@bottom: darken(@base, 15%);
+
+ @text-color-light: #f2f2f2;
+ @text-color-dark: #333333;
+ @text-color: contrast(@base, @text-color-dark, @text-color-light);
background-color: @base; /* fallback color if gradients are not supported */
background-image: -webkit-linear-gradient(top, @top, @bottom); /* For Chrome and Safari */
@@ -23,6 +58,10 @@ body {
.brand, .nav>li>a {
.navbar-inner-text(@base);
}
+
+ .brand {
+ .brand(@text-color, @text-color-dark, @text-color-light);
+ }
.nav {
li.dropdown.open>.dropdown-toggle, li.dropdown.active>.dropdown-toggle, li.dropdown.open.active>.dropdown-toggle {
@@ -65,9 +104,16 @@ body {
@base: #7728FF;
.navbar-inner-color(@base);
}
-
- .brand img {
- vertical-align: bottom;
+ &.black {
+ @base: #383838;
+ .navbar-inner-color(@base);
+ }
+
+ .brand span {
+ background-size: 20px 20px;
+ background-position: left center;
+ padding-left: 24px;
+ background-repeat: no-repeat;
}
}
@@ -111,10 +157,10 @@ body {
.octoprint-container {
.accordion-heading {
- .settings-trigger {
+ .accordion-heading-button {
float: right;
- .dropdown-toggle {
+ a {
display: inline-block;
padding: 8px 15px;
font-size: 14px;
@@ -145,18 +191,27 @@ body {
padding-right: 4px;
}
+.upload-buttons .btn {
+ margin-right: 0;
+}
/** Tables */
table {
table-layout: fixed;
+ .popover-title {
+ text-overflow: ellipsis;
+ word-break: break-all;
+ }
+
th, td {
overflow: hidden;
// gcode files
&.gcode_files_name {
text-overflow: ellipsis;
+ white-space: nowrap;
text-align: left;
}
@@ -309,6 +364,21 @@ ul.dropdown-menu li a {
#webcam_container {
width: 100%;
+
+ .flipH {
+ -webkit-transform: scaleX(-1);
+ -moz-transform: scaleX(-1);
+ }
+
+ .flipV {
+ -webkit-transform: scaleY(-1);
+ -moz-transform: scaleY(-1);
+ }
+
+ .flipH.flipV {
+ -webkit-transform: scaleX(-1) scaleY(-1);
+ -moz-transform: scaleX(-1) scaleY(-1);
+ }
}
/** GCODE file manager */
@@ -321,6 +391,10 @@ ul.dropdown-menu li a {
margin-bottom: 0;
}
}
+
+ table {
+ margin-bottom: 0;
+ }
}
/** Control tab */
@@ -371,21 +445,36 @@ ul.dropdown-menu li a {
}
+/** Terminal output */
+
+#term {
+ #terminal-output {
+ min-height: 340px;
+ }
+}
+
/** Settings dialog */
#settings_dialog {
}
/** Footer */
.footer {
- text-align: right;
+ ul {
+ margin: 0;
- ul li {
- display: inline;
- margin-left: 1em;
- font-size: 85%;
- a {
- color: #555;
+ li {
+ &:first-child {
+ margin-left: 0;
+ }
+
+ display: inline;
+ margin-left: 1em;
+ font-size: 85%;
+ a {
+ color: #555;
+ }
}
+
}
}
@@ -399,3 +488,133 @@ ul.dropdown-menu li a {
overflow: visible !important;
}
+#drop_overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 10000;
+ display: none;
+
+ &.in {
+ display: block;
+ }
+
+ #drop_overlay_background {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: #000000;
+ filter:alpha(opacity=50);
+ -moz-opacity:0.5;
+ -khtml-opacity: 0.5;
+ opacity: 0.5;
+ }
+
+ #drop_overlay_wrapper {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ padding-top: 60px;
+
+ @dropzone_width: 400px;
+ @dropzone_height: 400px;
+ @dropzone_distance: 50px;
+ @dropzone_border: 2px;
+
+ #drop, #drop_background {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ margin-left: -1 * @dropzone_width / 2;
+ margin-top: -1 * @dropzone_height / 2;
+ }
+
+ #drop_locally, #drop_locally_background {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ margin-left: -1 * @dropzone_width - @dropzone_distance / 2;
+ margin-top: -1 * @dropzone_height / 2;
+ }
+
+ #drop_sd, #drop_sd_background {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ margin-left: @dropzone_distance / 2;
+ margin-top: -1 * @dropzone_height / 2;
+ }
+
+ .dropzone {
+ width: @dropzone_width + 2 * @dropzone_border;
+ height: @dropzone_height + 2 * @dropzone_border;
+ z-index: 10001;
+
+ color: #ffffff;
+ font-size: 30px;
+
+ i {
+ font-size: 50px;
+ }
+
+ .centered {
+ display: table-cell;
+ text-align: center;
+ vertical-align: middle;
+ width: @dropzone_width;
+ height: @dropzone_height;
+ line-height: 40px;
+
+ filter:alpha(opacity=100);
+ -moz-opacity:1.0;
+ -khtml-opacity: 1.0;
+ opacity: 1.0;
+ }
+ }
+
+ .dropzone_background {
+ width: @dropzone_width;
+ height: @dropzone_height;
+ border: 2px dashed #eeeeee;
+ -webkit-border-radius: 10px;
+ -moz-border-radius: 10px;
+ border-radius: 10px;
+
+ background-color: #000000;
+ filter:alpha(opacity=25);
+ -moz-opacity:0.25;
+ -khtml-opacity: 0.25;
+ opacity: 0.25;
+
+ &.hover {
+ background-color: #000000;
+ filter:alpha(opacity=50);
+ -moz-opacity:0.5;
+ -khtml-opacity: 0.5;
+ opacity: 0.5;
+
+ }
+
+ &.fade {
+ -webkit-transition: all 0.3s ease-out;
+ -moz-transition: all 0.3s ease-out;
+ -ms-transition: all 0.3s ease-out;
+ -o-transition: all 0.3s ease-out;
+ transition: all 0.3s ease-out;
+ opacity: 1;
+ }
+ }
+ }
+}
+
+.icon-sd-black-14 {
+ background: url("../img/icon-sd-black-14.png") 0 3px no-repeat;
+ width: 11px;
+ height: 17px;
+}
diff --git a/octoprint/static/gcodeviewer/js/Worker.js b/octoprint/static/gcodeviewer/js/Worker.js
index 98b39b2..feb4d95 100644
--- a/octoprint/static/gcodeviewer/js/Worker.js
+++ b/octoprint/static/gcodeviewer/js/Worker.js
@@ -223,42 +223,31 @@
var assumeNonDC = false;
for(var i=0;i0&&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 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();
-
};
diff --git a/octoprint/static/gcodeviewer/js/gCodeReader.js b/octoprint/static/gcodeviewer/js/gCodeReader.js
index 190bc57..f30fdc6 100644
--- a/octoprint/static/gcodeviewer/js/gCodeReader.js
+++ b/octoprint/static/gcodeviewer/js/gCodeReader.js
@@ -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 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;iover the next couple of minutes, however you are welcome to try a manual reconnect " +
+ "anytime using the button below."
+ );
+ if (!$("#offline_overlay").is(":visible"))
+ $("#offline_overlay").show();
+
+ if (self._autoReconnectTrial < self._autoReconnectTimeouts.length) {
+ var timeout = self._autoReconnectTimeouts[self._autoReconnectTrial];
+ console.log("Reconnect trial #" + self._autoReconnectTrial + ", waiting " + timeout + "s");
+ setTimeout(self.reconnect, timeout * 1000);
+ self._autoReconnectTrial++;
+ } else {
+ self._onreconnectfailed();
+ }
+ }
+
+ self._onreconnectfailed = function() {
+ $("#offline_overlay_message").html(
+ "The server appears to be offline, at least I'm not getting any response from it. I could not reconnect automatically, " +
+ "but you may try a manual reconnect using the button below."
+ );
+ }
+
+ self._onmessage = function(e) {
+ for (var prop in e.data) {
+ var payload = e.data[prop];
+
+ switch (prop) {
+ case "history": {
+ self.connectionViewModel.fromHistoryData(payload);
+ self.printerStateViewModel.fromHistoryData(payload);
+ self.temperatureViewModel.fromHistoryData(payload);
+ self.controlViewModel.fromHistoryData(payload);
+ self.terminalViewModel.fromHistoryData(payload);
+ self.timelapseViewModel.fromHistoryData(payload);
+ self.gcodeViewModel.fromHistoryData(payload);
+ self.gcodeFilesViewModel.fromCurrentData(payload);
+ break;
+ }
+ case "current": {
+ self.connectionViewModel.fromCurrentData(payload);
+ self.printerStateViewModel.fromCurrentData(payload);
+ self.temperatureViewModel.fromCurrentData(payload);
+ self.controlViewModel.fromCurrentData(payload);
+ self.terminalViewModel.fromCurrentData(payload);
+ self.timelapseViewModel.fromCurrentData(payload);
+ self.gcodeViewModel.fromCurrentData(payload);
+ self.gcodeFilesViewModel.fromCurrentData(payload);
+ break;
+ }
+ case "updateTrigger": {
+ if (payload == "gcodeFiles") {
+ gcodeFilesViewModel.requestData();
+ } else if (payload == "timelapseFiles") {
+ timelapseViewModel.requestData();
+ }
+ break;
+ }
+ case "feedbackCommandOutput": {
+ self.controlViewModel.fromFeedbackCommandData(payload);
+ break;
+ }
+ case "timelapse": {
+ self.printerStateViewModel.fromTimelapseData(payload);
+ break;
+ }
+ }
+ }
+ }
+
+ self.connect();
+}
diff --git a/octoprint/static/js/app/helpers.js b/octoprint/static/js/app/helpers.js
new file mode 100644
index 0000000..16a7ee3
--- /dev/null
+++ b/octoprint/static/js/app/helpers.js
@@ -0,0 +1,275 @@
+function ItemListHelper(listType, supportedSorting, supportedFilters, defaultSorting, defaultFilters, exclusiveFilters, filesPerPage) {
+ var self = this;
+
+ self.listType = listType;
+ self.supportedSorting = supportedSorting;
+ self.supportedFilters = supportedFilters;
+ self.defaultSorting = defaultSorting;
+ self.defaultFilters = defaultFilters;
+ self.exclusiveFilters = exclusiveFilters;
+
+ self.allItems = [];
+
+ self.items = ko.observableArray([]);
+ self.pageSize = ko.observable(filesPerPage);
+ self.currentPage = ko.observable(0);
+ self.currentSorting = ko.observable(self.defaultSorting);
+ self.currentFilters = ko.observableArray(self.defaultFilters);
+ self.selectedItem = ko.observable(undefined);
+
+ //~~ item handling
+
+ self.updateItems = function(items) {
+ self.allItems = items;
+ self._updateItems();
+ }
+
+ self.selectItem = function(matcher) {
+ var itemList = self.items();
+ for (var i = 0; i < itemList.length; i++) {
+ if (matcher(itemList[i])) {
+ self.selectedItem(itemList[i]);
+ break;
+ }
+ }
+ }
+
+ self.selectNone = function() {
+ self.selectedItem(undefined);
+ }
+
+ self.isSelected = function(data) {
+ return self.selectedItem() == data;
+ }
+
+ self.isSelectedByMatcher = function(matcher) {
+ return matcher(self.selectedItem());
+ }
+
+ //~~ pagination
+
+ self.paginatedItems = ko.dependentObservable(function() {
+ if (self.items() == undefined) {
+ return [];
+ } else {
+ var from = Math.max(self.currentPage() * self.pageSize(), 0);
+ var to = Math.min(from + self.pageSize(), self.items().length);
+ return self.items().slice(from, to);
+ }
+ })
+ self.lastPage = ko.dependentObservable(function() {
+ return Math.ceil(self.items().length / self.pageSize()) - 1;
+ })
+ self.pages = ko.dependentObservable(function() {
+ var pages = [];
+ if (self.lastPage() < 7) {
+ for (var i = 0; i < self.lastPage() + 1; i++) {
+ pages.push({ number: i, text: i+1 });
+ }
+ } else {
+ pages.push({ number: 0, text: 1 });
+ if (self.currentPage() < 5) {
+ for (var i = 1; i < 5; i++) {
+ pages.push({ number: i, text: i+1 });
+ }
+ pages.push({ number: -1, text: "…"});
+ } else if (self.currentPage() > self.lastPage() - 5) {
+ pages.push({ number: -1, text: "…"});
+ for (var i = self.lastPage() - 4; i < self.lastPage(); i++) {
+ pages.push({ number: i, text: i+1 });
+ }
+ } else {
+ pages.push({ number: -1, text: "…"});
+ for (var i = self.currentPage() - 1; i <= self.currentPage() + 1; i++) {
+ pages.push({ number: i, text: i+1 });
+ }
+ pages.push({ number: -1, text: "…"});
+ }
+ pages.push({ number: self.lastPage(), text: self.lastPage() + 1})
+ }
+ return pages;
+ })
+
+ self.switchToItem = function(matcher) {
+ var pos = -1;
+ var itemList = self.items();
+ for (var i = 0; i < itemList.length; i++) {
+ if (matcher(itemList[i])) {
+ pos = i;
+ break;
+ }
+ }
+
+ if (pos > -1) {
+ var page = Math.floor(pos / self.pageSize());
+ self.changePage(page);
+ }
+ }
+
+ self.changePage = function(newPage) {
+ if (newPage < 0 || newPage > self.lastPage())
+ return;
+ self.currentPage(newPage);
+ }
+ self.prevPage = function() {
+ if (self.currentPage() > 0) {
+ self.currentPage(self.currentPage() - 1);
+ }
+ }
+ self.nextPage = function() {
+ if (self.currentPage() < self.lastPage()) {
+ self.currentPage(self.currentPage() + 1);
+ }
+ }
+
+ self.getItem = function(matcher) {
+ var itemList = self.items();
+ for (var i = 0; i < itemList.length; i++) {
+ if (matcher(itemList[i])) {
+ return itemList[i];
+ }
+ }
+
+ return undefined;
+ }
+
+ //~~ sorting
+
+ self.changeSorting = function(sorting) {
+ if (!_.contains(_.keys(self.supportedSorting), sorting))
+ return;
+
+ self.currentSorting(sorting);
+ self._saveCurrentSortingToLocalStorage();
+
+ self.changePage(0);
+ self._updateItems();
+ }
+
+ //~~ filtering
+
+ self.toggleFilter = function(filter) {
+ if (!_.contains(_.keys(self.supportedFilters), filter))
+ return;
+
+ if (_.contains(self.currentFilters(), filter)) {
+ self.removeFilter(filter);
+ } else {
+ self.addFilter(filter);
+ }
+ }
+
+ self.addFilter = function(filter) {
+ if (!_.contains(_.keys(self.supportedFilters), filter))
+ return;
+
+ for (var i = 0; i < self.exclusiveFilters.length; i++) {
+ if (_.contains(self.exclusiveFilters[i], filter)) {
+ for (var j = 0; j < self.exclusiveFilters[i].length; j++) {
+ if (self.exclusiveFilters[i][j] == filter)
+ continue;
+ self.removeFilter(self.exclusiveFilters[i][j]);
+ }
+ }
+ }
+
+ var filters = self.currentFilters();
+ filters.push(filter);
+ self.currentFilters(filters);
+ self._saveCurrentFiltersToLocalStorage();
+
+ self.changePage(0);
+ self._updateItems();
+ }
+
+ self.removeFilter = function(filter) {
+ if (!_.contains(_.keys(self.supportedFilters), filter))
+ return;
+
+ var filters = self.currentFilters();
+ filters.pop(filter);
+ self.currentFilters(filters);
+ self._saveCurrentFiltersToLocalStorage();
+
+ self.changePage(0);
+ self._updateItems();
+ }
+
+ //~~ update for sorted and filtered view
+
+ self._updateItems = function() {
+ // determine comparator
+ var comparator = undefined;
+ var currentSorting = self.currentSorting();
+ if (typeof currentSorting !== undefined && typeof self.supportedSorting[currentSorting] !== undefined) {
+ comparator = self.supportedSorting[currentSorting];
+ }
+
+ // work on all items
+ var result = self.allItems;
+
+ // filter if necessary
+ var filters = self.currentFilters();
+ _.each(filters, function(filter) {
+ if (typeof filter !== undefined && typeof supportedFilters[filter] !== undefined)
+ result = _.filter(result, supportedFilters[filter]);
+ });
+
+ // sort if necessary
+ if (typeof comparator !== undefined)
+ result.sort(comparator);
+
+ // set result list
+ self.items(result);
+ }
+
+ //~~ local storage
+
+ self._saveCurrentSortingToLocalStorage = function() {
+ if ( self._initializeLocalStorage() ) {
+ var currentSorting = self.currentSorting();
+ if (currentSorting !== undefined)
+ localStorage[self.listType + "." + "currentSorting"] = currentSorting;
+ else
+ localStorage[self.listType + "." + "currentSorting"] = undefined;
+ }
+ }
+
+ self._loadCurrentSortingFromLocalStorage = function() {
+ if ( self._initializeLocalStorage() ) {
+ if (_.contains(_.keys(supportedSorting), localStorage[self.listType + "." + "currentSorting"]))
+ self.currentSorting(localStorage[self.listType + "." + "currentSorting"]);
+ else
+ self.currentSorting(defaultSorting);
+ }
+ }
+
+ self._saveCurrentFiltersToLocalStorage = function() {
+ if ( self._initializeLocalStorage() ) {
+ var filters = _.intersection(_.keys(self.supportedFilters), self.currentFilters());
+ localStorage[self.listType + "." + "currentFilters"] = JSON.stringify(filters);
+ }
+ }
+
+ self._loadCurrentFiltersFromLocalStorage = function() {
+ 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 true;
+
+ localStorage[self.listType + "." + "currentSorting"] = self.defaultSorting;
+ localStorage[self.listType + "." + "currentFilters"] = JSON.stringify(self.defaultFilters);
+
+ return true;
+ }
+
+ self._loadCurrentFiltersFromLocalStorage();
+ self._loadCurrentSortingFromLocalStorage();
+}
diff --git a/octoprint/static/js/app/main.js b/octoprint/static/js/app/main.js
new file mode 100644
index 0000000..795be04
--- /dev/null
+++ b/octoprint/static/js/app/main.js
@@ -0,0 +1,333 @@
+$(function() {
+
+ //~~ Initialize view models
+ var loginStateViewModel = new LoginStateViewModel();
+ var usersViewModel = new UsersViewModel(loginStateViewModel);
+ var settingsViewModel = new SettingsViewModel(loginStateViewModel, usersViewModel);
+ var connectionViewModel = new ConnectionViewModel(loginStateViewModel, settingsViewModel);
+ var timelapseViewModel = new TimelapseViewModel(loginStateViewModel);
+ var printerStateViewModel = new PrinterStateViewModel(loginStateViewModel, timelapseViewModel);
+ var appearanceViewModel = new AppearanceViewModel(settingsViewModel);
+ var temperatureViewModel = new TemperatureViewModel(loginStateViewModel, settingsViewModel);
+ var controlViewModel = new ControlViewModel(loginStateViewModel, settingsViewModel);
+ var terminalViewModel = new TerminalViewModel(loginStateViewModel, settingsViewModel);
+ var gcodeFilesViewModel = new GcodeFilesViewModel(printerStateViewModel, loginStateViewModel);
+ var gcodeViewModel = new GcodeViewModel(loginStateViewModel);
+ var navigationViewModel = new NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsViewModel, usersViewModel);
+
+ var dataUpdater = new DataUpdater(
+ loginStateViewModel,
+ connectionViewModel,
+ printerStateViewModel,
+ temperatureViewModel,
+ controlViewModel,
+ terminalViewModel,
+ gcodeFilesViewModel,
+ timelapseViewModel,
+ gcodeViewModel
+ );
+
+ // work around a stupid iOS6 bug where ajax requests get cached and only work once, as described at
+ // http://stackoverflow.com/questions/12506897/is-safari-on-ios-6-caching-ajax-results
+ $.ajaxSetup({
+ type: 'POST',
+ headers: { "cache-control": "no-cache" }
+ });
+
+ //~~ Show settings - to ensure centered
+ $('#navbar_show_settings').click(function() {
+ $('#settings_dialog').modal()
+ .css({
+ width: 'auto',
+ 'margin-left': function() { return -($(this).width() /2); }
+ });
+ return false;
+ });
+
+ //~~ Temperature
+
+ $('#tabs a[data-toggle="tab"]').on('shown', function (e) {
+ temperatureViewModel.updatePlot();
+ terminalViewModel.updateOutput();
+ });
+
+ //~~ Gcode upload
+
+ function gcode_upload_done(e, data) {
+ gcodeFilesViewModel.fromResponse(data.result);
+ $("#gcode_upload_progress .bar").css("width", "0%");
+ $("#gcode_upload_progress").removeClass("progress-striped").removeClass("active");
+ $("#gcode_upload_progress .bar").text("");
+ }
+
+ function gcode_upload_fail(e, data) {
+ $.pnotify({
+ title: "Upload failed",
+ text: "Could not upload the file. Make sure it is a GCODE file and has one of the following extensions: .gcode, .gco
Server reported:
" + data.jqXHR.responseText + "
",
+ type: "error",
+ hide: false
+ });
+ $("#gcode_upload_progress .bar").css("width", "0%");
+ $("#gcode_upload_progress").removeClass("progress-striped").removeClass("active");
+ $("#gcode_upload_progress .bar").text("");
+ }
+
+ function gcode_upload_progress(e, data) {
+ var progress = parseInt(data.loaded / data.total * 100, 10);
+ $("#gcode_upload_progress .bar").css("width", progress + "%");
+ $("#gcode_upload_progress .bar").text("Uploading ...");
+ if (progress >= 100) {
+ $("#gcode_upload_progress").addClass("progress-striped").addClass("active");
+ $("#gcode_upload_progress .bar").text("Saving ...");
+ }
+ }
+
+ function enable_local_dropzone() {
+ $("#gcode_upload").fileupload({
+ dataType: "json",
+ dropZone: localTarget,
+ formData: {target: "local"},
+ done: gcode_upload_done,
+ fail: gcode_upload_fail,
+ progressall: gcode_upload_progress
+ });
+ }
+
+ function disable_local_dropzone() {
+ $("#gcode_upload").fileupload({
+ dataType: "json",
+ dropZone: null,
+ formData: {target: "local"},
+ done: gcode_upload_done,
+ fail: gcode_upload_fail,
+ progressall: gcode_upload_progress
+ });
+ }
+
+ function enable_sd_dropzone() {
+ $("#gcode_upload_sd").fileupload({
+ dataType: "json",
+ dropZone: $("#drop_sd"),
+ formData: {target: "sd"},
+ done: gcode_upload_done,
+ fail: gcode_upload_fail,
+ progressall: gcode_upload_progress
+ });
+ }
+
+ function disable_sd_dropzone() {
+ $("#gcode_upload_sd").fileupload({
+ dataType: "json",
+ dropZone: null,
+ formData: {target: "sd"},
+ done: gcode_upload_done,
+ fail: gcode_upload_fail,
+ progressall: gcode_upload_progress
+ });
+ }
+
+ var localTarget;
+ if (CONFIG_SD_SUPPORT) {
+ localTarget = $("#drop_locally");
+ } else {
+ localTarget = $("#drop");
+ }
+
+ loginStateViewModel.isUser.subscribe(function(newValue) {
+ if (newValue === true) {
+ enable_local_dropzone();
+ } else {
+ disable_local_dropzone();
+ }
+ });
+
+ if (loginStateViewModel.isUser()) {
+ enable_local_dropzone();
+ } else {
+ disable_local_dropzone();
+ }
+
+ if (CONFIG_SD_SUPPORT) {
+ printerStateViewModel.isSdReady.subscribe(function(newValue) {
+ if (newValue === true && loginStateViewModel.isUser()) {
+ enable_sd_dropzone();
+ } else {
+ disable_sd_dropzone();
+ }
+ });
+
+ loginStateViewModel.isUser.subscribe(function(newValue) {
+ if (newValue === true && printerStateViewModel.isSdReady()) {
+ enable_sd_dropzone();
+ } else {
+ disable_sd_dropzone();
+ }
+ });
+
+ if (printerStateViewModel.isSdReady() && loginStateViewModel.isUser()) {
+ enable_sd_dropzone();
+ } else {
+ disable_sd_dropzone();
+ }
+ }
+
+ $(document).bind("dragover", function (e) {
+ var dropOverlay = $("#drop_overlay");
+ var dropZone = $("#drop");
+ var dropZoneLocal = $("#drop_locally");
+ var dropZoneSd = $("#drop_sd");
+ var dropZoneBackground = $("#drop_background");
+ var dropZoneLocalBackground = $("#drop_locally_background");
+ var dropZoneSdBackground = $("#drop_sd_background");
+ var timeout = window.dropZoneTimeout;
+
+ if (!timeout) {
+ dropOverlay.addClass('in');
+ } else {
+ clearTimeout(timeout);
+ }
+
+ var foundLocal = false;
+ var foundSd = false;
+ var found = false
+ var node = e.target;
+ do {
+ if (dropZoneLocal && node === dropZoneLocal[0]) {
+ foundLocal = true;
+ break;
+ } else if (dropZoneSd && node === dropZoneSd[0]) {
+ foundSd = true;
+ break;
+ } else if (dropZone && node === dropZone[0]) {
+ found = true;
+ break;
+ }
+ node = node.parentNode;
+ } while (node != null);
+
+ if (foundLocal) {
+ dropZoneLocalBackground.addClass("hover");
+ dropZoneSdBackground.removeClass("hover");
+ } else if (foundSd && printerStateViewModel.isSdReady()) {
+ dropZoneSdBackground.addClass("hover");
+ dropZoneLocalBackground.removeClass("hover");
+ } else if (found) {
+ dropZoneBackground.addClass("hover");
+ } else {
+ if (dropZoneLocalBackground) dropZoneLocalBackground.removeClass("hover");
+ if (dropZoneSdBackground) dropZoneSdBackground.removeClass("hover");
+ if (dropZoneBackground) dropZoneBackground.removeClass("hover");
+ }
+
+ window.dropZoneTimeout = setTimeout(function () {
+ window.dropZoneTimeout = null;
+ dropOverlay.removeClass("in");
+ if (dropZoneLocal) dropZoneLocalBackground.removeClass("hover");
+ if (dropZoneSd) dropZoneSdBackground.removeClass("hover");
+ if (dropZone) dropZoneBackground.removeClass("hover");
+ }, 100);
+ });
+
+ //~~ Offline overlay
+ $("#offline_overlay_reconnect").click(function() {dataUpdater.reconnect()});
+
+ //~~ knockout.js bindings
+
+ ko.bindingHandlers.popover = {
+ init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
+ var val = ko.utils.unwrapObservable(valueAccessor());
+
+ var options = {
+ title: val.title,
+ animation: val.animation,
+ placement: val.placement,
+ trigger: val.trigger,
+ delay: val.delay,
+ content: val.content,
+ html: val.html
+ };
+ $(element).popover(options);
+ }
+ }
+
+ ko.applyBindings(connectionViewModel, document.getElementById("connection_accordion"));
+ ko.applyBindings(printerStateViewModel, document.getElementById("state_accordion"));
+ ko.applyBindings(gcodeFilesViewModel, document.getElementById("files_accordion"));
+ ko.applyBindings(temperatureViewModel, document.getElementById("temp"));
+ ko.applyBindings(controlViewModel, document.getElementById("control"));
+ ko.applyBindings(terminalViewModel, document.getElementById("term"));
+ var gcode = document.getElementById("gcode");
+ if (gcode) {
+ ko.applyBindings(gcodeViewModel, gcode);
+ }
+ ko.applyBindings(settingsViewModel, document.getElementById("settings_dialog"));
+ ko.applyBindings(navigationViewModel, document.getElementById("navbar"));
+ ko.applyBindings(appearanceViewModel, document.getElementsByTagName("head")[0]);
+ ko.applyBindings(printerStateViewModel, document.getElementById("drop_overlay"));
+
+ var timelapseElement = document.getElementById("timelapse");
+ if (timelapseElement) {
+ ko.applyBindings(timelapseViewModel, timelapseElement);
+ }
+ var gCodeVisualizerElement = document.getElementById("gcode");
+ if (gCodeVisualizerElement) {
+ gcodeViewModel.initialize();
+ }
+
+ //~~ startup commands
+
+ loginStateViewModel.requestData();
+ connectionViewModel.requestData();
+ controlViewModel.requestData();
+ gcodeFilesViewModel.requestData();
+ timelapseViewModel.requestData();
+
+ loginStateViewModel.subscribe(function(change, data) {
+ if ("login" == change) {
+ $("#gcode_upload").fileupload("enable");
+
+ settingsViewModel.requestData();
+ if (data.admin) {
+ usersViewModel.requestData();
+ }
+ } else {
+ $("#gcode_upload").fileupload("disable");
+ }
+ });
+
+ //~~ UI stuff
+
+ $(".accordion-toggle[href='#files']").click(function() {
+ if ($("#files").hasClass("in")) {
+ $("#files").removeClass("overflow_visible");
+ } else {
+ setTimeout(function() {
+ $("#files").addClass("overflow_visible");
+ }, 1000);
+ }
+ })
+
+ $.pnotify.defaults.history = false;
+
+ $.fn.modal.defaults.maxHeight = function(){
+ // subtract the height of the modal header and footer
+ return $(window).height() - 165;
+ }
+
+ // Fix input element click problem on login dialog
+ $(".dropdown input, .dropdown label").click(function(e) {
+ e.stopPropagation();
+ });
+
+ $(document).bind("drop dragover", function (e) {
+ e.preventDefault();
+ });
+
+ if (CONFIG_FIRST_RUN) {
+ var firstRunViewModel = new FirstRunViewModel();
+ ko.applyBindings(firstRunViewModel, document.getElementById("first_run_dialog"));
+ firstRunViewModel.showDialog();
+ }
+ }
+);
+
diff --git a/octoprint/static/js/app/viewmodels/appearance.js b/octoprint/static/js/app/viewmodels/appearance.js
new file mode 100644
index 0000000..fc09c9e
--- /dev/null
+++ b/octoprint/static/js/app/viewmodels/appearance.js
@@ -0,0 +1,20 @@
+function AppearanceViewModel(settingsViewModel) {
+ var self = this;
+
+ self.name = settingsViewModel.appearance_name;
+ self.color = settingsViewModel.appearance_color;
+
+ self.brand = ko.computed(function() {
+ if (self.name())
+ return "OctoPrint: " + self.name();
+ else
+ return "OctoPrint";
+ })
+
+ self.title = ko.computed(function() {
+ if (self.name())
+ return self.name() + " [OctoPrint]";
+ else
+ return "OctoPrint";
+ })
+}
diff --git a/octoprint/static/js/app/viewmodels/connection.js b/octoprint/static/js/app/viewmodels/connection.js
new file mode 100644
index 0000000..9baa9c4
--- /dev/null
+++ b/octoprint/static/js/app/viewmodels/connection.js
@@ -0,0 +1,115 @@
+function ConnectionViewModel(loginStateViewModel, settingsViewModel) {
+ var self = this;
+
+ self.loginState = loginStateViewModel;
+ self.settings = settingsViewModel;
+
+ self.portOptions = ko.observableArray(undefined);
+ self.baudrateOptions = ko.observableArray(undefined);
+ self.selectedPort = ko.observable(undefined);
+ self.selectedBaudrate = ko.observable(undefined);
+ self.saveSettings = ko.observable(undefined);
+
+ self.isErrorOrClosed = ko.observable(undefined);
+ self.isOperational = ko.observable(undefined);
+ self.isPrinting = ko.observable(undefined);
+ self.isPaused = ko.observable(undefined);
+ self.isError = ko.observable(undefined);
+ self.isReady = ko.observable(undefined);
+ self.isLoading = ko.observable(undefined);
+
+ self.buttonText = ko.computed(function() {
+ if (self.isErrorOrClosed())
+ return "Connect";
+ else
+ return "Disconnect";
+ })
+
+ self.previousIsOperational = undefined;
+
+ self.requestData = function() {
+ $.ajax({
+ url: AJAX_BASEURL + "control/connection/options",
+ method: "GET",
+ dataType: "json",
+ success: function(response) {
+ self.fromResponse(response);
+ }
+ })
+ }
+
+ self.fromResponse = function(response) {
+ self.portOptions(response.ports);
+ self.baudrateOptions(response.baudrates);
+
+ if (!self.selectedPort() && response.ports && response.ports.indexOf(response.portPreference) >= 0)
+ self.selectedPort(response.portPreference);
+ if (!self.selectedBaudrate() && response.baudrates && response.baudrates.indexOf(response.baudratePreference) >= 0)
+ self.selectedBaudrate(response.baudratePreference);
+
+ self.saveSettings(false);
+ }
+
+ self.fromHistoryData = function(data) {
+ self._processStateData(data.state);
+ }
+
+ self.fromCurrentData = function(data) {
+ self._processStateData(data.state);
+ }
+
+ self._processStateData = function(data) {
+ self.previousIsOperational = self.isOperational();
+
+ self.isErrorOrClosed(data.flags.closedOrError);
+ self.isOperational(data.flags.operational);
+ self.isPaused(data.flags.paused);
+ self.isPrinting(data.flags.printing);
+ self.isError(data.flags.error);
+ self.isReady(data.flags.ready);
+ self.isLoading(data.flags.loading);
+
+ var connectionTab = $("#connection");
+ if (self.previousIsOperational != self.isOperational()) {
+ if (self.isOperational() && connectionTab.hasClass("in")) {
+ // connection just got established, close connection tab for now
+ connectionTab.collapse("hide");
+ } else if (!connectionTab.hasClass("in")) {
+ // connection just dropped, make sure connection tab is open
+ connectionTab.collapse("show");
+ }
+ }
+ }
+
+ self.connect = function() {
+ if (self.isErrorOrClosed()) {
+ var data = {
+ "command": "connect",
+ "port": self.selectedPort(),
+ "baudrate": self.selectedBaudrate()
+ };
+
+ if (self.saveSettings())
+ data["save"] = true;
+
+ $.ajax({
+ url: AJAX_BASEURL + "control/connection",
+ type: "POST",
+ dataType: "json",
+ data: data
+ })
+
+ self.settings.serial_port(self.selectedPort())
+ self.settings.serial_baudrate(self.selectedBaudrate())
+ self.settings.saveData();
+ } else {
+ self.requestData();
+ $.ajax({
+ url: AJAX_BASEURL + "control/connection",
+ type: "POST",
+ dataType: "json",
+ data: {"command": "disconnect"}
+ })
+ }
+ }
+}
diff --git a/octoprint/static/js/app/viewmodels/control.js b/octoprint/static/js/app/viewmodels/control.js
new file mode 100644
index 0000000..3f89d57
--- /dev/null
+++ b/octoprint/static/js/app/viewmodels/control.js
@@ -0,0 +1,172 @@
+function ControlViewModel(loginStateViewModel, settingsViewModel) {
+ var self = this;
+
+ self.loginState = loginStateViewModel;
+ self.settings = settingsViewModel;
+
+ self.isErrorOrClosed = ko.observable(undefined);
+ self.isOperational = ko.observable(undefined);
+ self.isPrinting = ko.observable(undefined);
+ self.isPaused = ko.observable(undefined);
+ self.isError = ko.observable(undefined);
+ self.isReady = ko.observable(undefined);
+ self.isLoading = ko.observable(undefined);
+
+ self.extrusionAmount = ko.observable(undefined);
+ self.controls = ko.observableArray([]);
+
+ self.feedbackControlLookup = {};
+
+ self.fromCurrentData = function(data) {
+ self._processStateData(data.state);
+ }
+
+ self.fromHistoryData = function(data) {
+ self._processStateData(data.state);
+ }
+
+ self._processStateData = function(data) {
+ self.isErrorOrClosed(data.flags.closedOrError);
+ self.isOperational(data.flags.operational);
+ self.isPaused(data.flags.paused);
+ self.isPrinting(data.flags.printing);
+ self.isError(data.flags.error);
+ self.isReady(data.flags.ready);
+ self.isLoading(data.flags.loading);
+ }
+
+ self.fromFeedbackCommandData = function(data) {
+ if (data.name in self.feedbackControlLookup) {
+ self.feedbackControlLookup[data.name](data.output);
+ }
+ }
+
+ self.requestData = function() {
+ $.ajax({
+ url: AJAX_BASEURL + "control/custom",
+ method: "GET",
+ dataType: "json",
+ success: function(response) {
+ self._fromResponse(response);
+ }
+ });
+ }
+
+ self._fromResponse = function(response) {
+ self.controls(self._processControls(response.controls));
+ }
+
+ self._processControls = function(controls) {
+ for (var i = 0; i < controls.length; i++) {
+ controls[i] = self._processControl(controls[i]);
+ }
+ return controls;
+ }
+
+ self._processControl = function(control) {
+ if (control.type == "parametric_command" || control.type == "parametric_commands") {
+ for (var i = 0; i < control.input.length; i++) {
+ control.input[i].value = control.input[i].default;
+ }
+ } else if (control.type == "feedback_command" || control.type == "feedback") {
+ control.output = ko.observable("");
+ self.feedbackControlLookup[control.name] = control.output;
+ } else if (control.type == "section") {
+ control.children = self._processControls(control.children);
+ }
+ return control;
+ }
+
+ self.sendJogCommand = function(axis, multiplier, distance) {
+ if (typeof distance === "undefined")
+ distance = $('#jog_distance button.active').data('distance');
+ $.ajax({
+ url: AJAX_BASEURL + "control/jog",
+ type: "POST",
+ dataType: "json",
+ data: axis + "=" + ( distance * multiplier )
+ })
+ }
+
+ self.sendHomeCommand = function(axis) {
+ $.ajax({
+ url: AJAX_BASEURL + "control/jog",
+ type: "POST",
+ dataType: "json",
+ data: "home" + axis
+ })
+ }
+
+ self.sendExtrudeCommand = function() {
+ self._sendECommand(1);
+ }
+
+ self.sendRetractCommand = function() {
+ self._sendECommand(-1);
+ }
+
+ self._sendECommand = function(dir) {
+ var length = self.extrusionAmount();
+ if (!length)
+ length = 5;
+ $.ajax({
+ url: AJAX_BASEURL + "control/jog",
+ type: "POST",
+ dataType: "json",
+ data: "extrude=" + (dir * length)
+ })
+ }
+
+ self.sendCustomCommand = function(command) {
+ if (!command)
+ return;
+
+ var data = undefined;
+ if (command.type == "command" || command.type == "parametric_command" || command.type == "feedback_command") {
+ // single command
+ data = {"command" : command.command};
+ } else if (command.type == "commands" || command.type == "parametric_commands") {
+ // multi command
+ data = {"commands": command.commands};
+ }
+
+ if (command.type == "parametric_command" || command.type == "parametric_commands") {
+ // parametric command(s)
+ data["parameters"] = {};
+ for (var i = 0; i < command.input.length; i++) {
+ data["parameters"][command.input[i].parameter] = command.input[i].value;
+ }
+ }
+
+ if (!data)
+ return;
+
+ $.ajax({
+ url: AJAX_BASEURL + "control/command",
+ type: "POST",
+ dataType: "json",
+ contentType: "application/json; charset=UTF-8",
+ data: JSON.stringify(data)
+ })
+ }
+
+ self.displayMode = function(customControl) {
+ switch (customControl.type) {
+ case "section":
+ return "customControls_sectionTemplate";
+ case "command":
+ case "commands":
+ return "customControls_commandTemplate";
+ case "parametric_command":
+ case "parametric_commands":
+ return "customControls_parametricCommandTemplate";
+ case "feedback_command":
+ return "customControls_feedbackCommandTemplate";
+ case "feedback":
+ return "customControls_feedbackTemplate";
+ default:
+ return "customControls_emptyTemplate";
+ }
+ }
+
+}
diff --git a/octoprint/static/js/app/viewmodels/firstrun.js b/octoprint/static/js/app/viewmodels/firstrun.js
new file mode 100644
index 0000000..8697547
--- /dev/null
+++ b/octoprint/static/js/app/viewmodels/firstrun.js
@@ -0,0 +1,75 @@
+function FirstRunViewModel() {
+ var self = this;
+
+ self.username = ko.observable(undefined);
+ self.password = ko.observable(undefined);
+ self.confirmedPassword = ko.observable(undefined);
+
+ self.passwordMismatch = ko.computed(function() {
+ return self.password() != self.confirmedPassword();
+ });
+
+ self.validUsername = ko.computed(function() {
+ return self.username() && self.username().trim() != "";
+ });
+
+ self.validPassword = ko.computed(function() {
+ return self.password() && self.password().trim() != "";
+ });
+
+ self.validData = ko.computed(function() {
+ return !self.passwordMismatch() && self.validUsername() && self.validPassword();
+ });
+
+ self.keepAccessControl = function() {
+ if (!self.validData()) return;
+
+ var data = {
+ "ac": true,
+ "user": self.username(),
+ "pass1": self.password(),
+ "pass2": self.confirmedPassword()
+ };
+ self._sendData(data);
+ };
+
+ self.disableAccessControl = function() {
+ $("#confirmation_dialog .confirmation_dialog_message").html("If you disable Access Control and your OctoPrint " +
+ "installation is accessible from the internet, your printer will be accessible by everyone - " +
+ "that also includes the bad guys!");
+ $("#confirmation_dialog .confirmation_dialog_acknowledge").click(function(e) {
+ e.preventDefault();
+ $("#confirmation_dialog").modal("hide");
+
+ var data = {
+ "ac": false
+ };
+ self._sendData(data, function() {
+ // if the user indeed disables access control, we'll need to reload the page for this to take effect
+ location.reload();
+ });
+ });
+ $("#confirmation_dialog").modal("show");
+ };
+
+ self._sendData = function(data, callback) {
+ $.ajax({
+ url: AJAX_BASEURL + "setup",
+ type: "POST",
+ dataType: "json",
+ data: data,
+ success: function() {
+ self.closeDialog();
+ if (callback) callback();
+ }
+ });
+ }
+
+ self.showDialog = function() {
+ $("#first_run_dialog").modal("show");
+ }
+
+ self.closeDialog = function() {
+ $("#first_run_dialog").modal("hide");
+ }
+}
diff --git a/octoprint/static/js/app/viewmodels/gcode.js b/octoprint/static/js/app/viewmodels/gcode.js
new file mode 100644
index 0000000..80f362a
--- /dev/null
+++ b/octoprint/static/js/app/viewmodels/gcode.js
@@ -0,0 +1,75 @@
+function GcodeViewModel(loginStateViewModel) {
+ var self = this;
+
+ self.loginState = loginStateViewModel;
+
+ self.loadedFilename = undefined;
+ self.loadedFileMTime = undefined;
+ self.status = 'idle';
+ self.enabled = false;
+
+ self.errorCount = 0;
+
+ self.initialize = function(){
+ self.enabled = true;
+ GCODE.ui.initHandlers();
+ }
+
+ self.loadFile = function(filename, mtime){
+ if (self.status == 'idle' && self.errorCount < 3) {
+ self.status = 'request';
+ $.ajax({
+ url: "/downloads/gcode/" + filename,
+ data: { "mtime": mtime },
+ type: "GET",
+ success: function(response, rstatus) {
+ if(rstatus === 'success'){
+ self.showGCodeViewer(response, rstatus);
+ self.loadedFilename=filename;
+ self.loadedFileMTime=mtime;
+ self.status = 'idle';
+ }
+ },
+ error: function() {
+ self.status = 'idle';
+ self.errorCount++;
+ }
+ })
+ }
+ }
+
+ self.showGCodeViewer = function(response, rstatus) {
+ var par = {};
+ par.target = {};
+ par.target.result = response;
+ GCODE.gCodeReader.loadFile(par);
+ }
+
+ self.fromHistoryData = function(data) {
+ self._processData(data);
+ }
+
+ self.fromCurrentData = function(data) {
+ self._processData(data);
+ }
+
+ self._processData = function(data) {
+ if (!self.enabled) return;
+ if (!data.job.filename) return;
+
+ if(self.loadedFilename && self.loadedFilename == data.job.filename &&
+ self.loadedFileMTime == data.job.mtime) {
+ 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) {
+ self.loadFile(data.job.filename, data.job.mtime);
+ }
+ }
+
+}
diff --git a/octoprint/static/js/app/viewmodels/gcodefiles.js b/octoprint/static/js/app/viewmodels/gcodefiles.js
new file mode 100644
index 0000000..fdf2c41
--- /dev/null
+++ b/octoprint/static/js/app/viewmodels/gcodefiles.js
@@ -0,0 +1,202 @@
+function GcodeFilesViewModel(printerStateViewModel, loginStateViewModel) {
+ var self = this;
+
+ self.printerState = printerStateViewModel;
+ self.loginState = loginStateViewModel;
+
+ self.isErrorOrClosed = ko.observable(undefined);
+ self.isOperational = ko.observable(undefined);
+ self.isPrinting = ko.observable(undefined);
+ self.isPaused = ko.observable(undefined);
+ self.isError = ko.observable(undefined);
+ self.isReady = ko.observable(undefined);
+ self.isLoading = ko.observable(undefined);
+ self.isSdReady = ko.observable(undefined);
+
+ self.freeSpace = ko.observable(undefined);
+
+ // initialize list helper
+ self.listHelper = new ItemListHelper(
+ "gcodeFiles",
+ {
+ "name": function(a, b) {
+ // sorts ascending
+ if (a["name"].toLocaleLowerCase() < b["name"].toLocaleLowerCase()) return -1;
+ if (a["name"].toLocaleLowerCase() > b["name"].toLocaleLowerCase()) return 1;
+ return 0;
+ },
+ "upload": function(a, b) {
+ // sorts descending
+ if (b["date"] === undefined || a["date"] > b["date"]) return -1;
+ if (a["date"] < b["date"]) return 1;
+ return 0;
+ },
+ "size": function(a, b) {
+ // sorts descending
+ if (b["bytes"] === undefined || a["bytes"] > b["bytes"]) return -1;
+ if (a["bytes"] < b["bytes"]) return 1;
+ return 0;
+ }
+ },
+ {
+ "printed": function(file) {
+ return !(file["prints"] && file["prints"]["success"] && file["prints"]["success"] > 0);
+ },
+ "sd": function(file) {
+ return file["origin"] && file["origin"] == "sd";
+ },
+ "local": function(file) {
+ return !(file["origin"] && file["origin"] == "sd");
+ }
+ },
+ "name",
+ [],
+ [["sd", "local"]],
+ CONFIG_GCODEFILESPERPAGE
+ );
+
+ self.isLoadActionPossible = ko.computed(function() {
+ return self.loginState.isUser() && !self.isPrinting() && !self.isPaused() && !self.isLoading();
+ });
+
+ self.isLoadAndPrintActionPossible = ko.computed(function() {
+ return self.loginState.isUser() && self.isOperational() && self.isLoadActionPossible();
+ });
+
+ self.printerState.filename.subscribe(function(newValue) {
+ self.highlightFilename(newValue);
+ });
+
+ self.highlightFilename = function(filename) {
+ if (filename == undefined) {
+ self.listHelper.selectNone();
+ } else {
+ self.listHelper.selectItem(function(item) {
+ return item.name == filename;
+ })
+ }
+ }
+
+ self.fromCurrentData = function(data) {
+ self._processStateData(data.state);
+ }
+
+ self.fromHistoryData = function(data) {
+ self._processStateData(data.state);
+ }
+
+ self._processStateData = function(data) {
+ self.isErrorOrClosed(data.flags.closedOrError);
+ self.isOperational(data.flags.operational);
+ self.isPaused(data.flags.paused);
+ 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.requestData = function() {
+ $.ajax({
+ url: AJAX_BASEURL + "gcodefiles",
+ method: "GET",
+ dataType: "json",
+ success: function(response) {
+ self.fromResponse(response);
+ }
+ });
+ }
+
+ self.fromResponse = function(response) {
+ self.listHelper.updateItems(response.files);
+
+ if (response.filename) {
+ // got a file to scroll to
+ self.listHelper.switchToItem(function(item) {return item.name == response.filename});
+ }
+
+ self.freeSpace(response.free);
+
+ self.highlightFilename(self.printerState.filename());
+ }
+
+ self.loadFile = function(filename, printAfterLoad) {
+ var file = self.listHelper.getItem(function(item) {return item.name == filename});
+ if (!file) return;
+
+ $.ajax({
+ url: AJAX_BASEURL + "gcodefiles/load",
+ type: "POST",
+ dataType: "json",
+ data: {filename: filename, print: printAfterLoad, target: file.origin}
+ })
+ }
+
+ self.removeFile = function(filename) {
+ var file = self.listHelper.getItem(function(item) {return item.name == filename});
+ if (!file) return;
+
+ $.ajax({
+ url: AJAX_BASEURL + "gcodefiles/delete",
+ type: "POST",
+ dataType: "json",
+ data: {filename: filename, target: file.origin},
+ success: self.fromResponse
+ })
+ }
+
+ self.initSdCard = function() {
+ self._sendSdCommand("init");
+ }
+
+ self.releaseSdCard = function() {
+ self._sendSdCommand("release");
+ }
+
+ self.refreshSdFiles = function() {
+ self._sendSdCommand("refresh");
+ }
+
+ self._sendSdCommand = function(command) {
+ $.ajax({
+ url: AJAX_BASEURL + "control/sd",
+ type: "POST",
+ dataType: "json",
+ data: {command: command}
+ });
+ }
+
+ self.getPopoverContent = function(data) {
+ var output = "Uploaded: " + data["date"] + "
";
+ if (data["gcodeAnalysis"]) {
+ output += "";
+ output += "Filament: " + data["gcodeAnalysis"]["filament"] + "
";
+ output += "Estimated Print Time: " + data["gcodeAnalysis"]["estimatedPrintTime"];
+ output += "
";
+ }
+ if (data["prints"] && data["prints"]["last"]) {
+ output += "";
+ output += "Last Print: " + data["prints"]["last"]["date"] + "";
+ output += "
";
+ }
+ return output;
+ }
+
+ self.getSuccessClass = function(data) {
+ if (!data["prints"] || !data["prints"]["last"]) {
+ return "";
+ }
+ return data["prints"]["last"]["success"] ? "text-success" : "text-error";
+ }
+
+ self.enableRemove = function(data) {
+ return self.loginState.isUser() && !(self.listHelper.isSelected(data) && (self.isPrinting() || self.isPaused()));
+ }
+
+ self.enableSelect = function(data, printAfterSelect) {
+ var isLoadActionPossible = self.loginState.isUser() && self.isOperational() && !(self.isPrinting() || self.isPaused() || self.isLoading());
+ return isLoadActionPossible && !self.listHelper.isSelected(data);
+ }
+
+}
+
diff --git a/octoprint/static/js/app/viewmodels/loginstate.js b/octoprint/static/js/app/viewmodels/loginstate.js
new file mode 100644
index 0000000..75882b6
--- /dev/null
+++ b/octoprint/static/js/app/viewmodels/loginstate.js
@@ -0,0 +1,89 @@
+function LoginStateViewModel() {
+ var self = this;
+
+ self.loggedIn = ko.observable(false);
+ self.username = ko.observable(undefined);
+ self.isAdmin = ko.observable(false);
+ self.isUser = ko.observable(false);
+
+ self.currentUser = ko.observable(undefined);
+
+ self.userMenuText = ko.computed(function() {
+ if (self.loggedIn()) {
+ return "\"" + self.username() + "\"";
+ } else {
+ return "Login";
+ }
+ })
+
+ self.subscribers = [];
+ self.subscribe = function(callback) {
+ if (callback === undefined) return;
+ self.subscribers.push(callback);
+ }
+
+ self.requestData = function() {
+ $.ajax({
+ url: AJAX_BASEURL + "login",
+ type: "POST",
+ data: {"passive": true},
+ success: self.fromResponse
+ })
+ }
+
+ self.fromResponse = function(response) {
+ if (response && response.name) {
+ self.loggedIn(true);
+ self.username(response.name);
+ self.isUser(response.user);
+ self.isAdmin(response.admin);
+
+ self.currentUser(response);
+
+ _.each(self.subscribers, function(callback) { callback("login", response); });
+ } else {
+ self.loggedIn(false);
+ self.username(undefined);
+ self.isUser(false);
+ self.isAdmin(false);
+
+ self.currentUser(undefined);
+
+ _.each(self.subscribers, function(callback) { callback("logout", {}); });
+ }
+ }
+
+ self.login = function() {
+ var username = $("#login_user").val();
+ var password = $("#login_pass").val();
+ var remember = $("#login_remember").is(":checked");
+
+ $("#login_user").val("");
+ $("#login_pass").val("");
+ $("#login_remember").prop("checked", false);
+
+ $.ajax({
+ url: AJAX_BASEURL + "login",
+ type: "POST",
+ data: {"user": username, "pass": password, "remember": remember},
+ success: function(response) {
+ $.pnotify({title: "Login successful", text: "You are now logged in as \"" + response.name + "\"", type: "success"});
+ self.fromResponse(response);
+ },
+ error: function(jqXHR, textStatus, errorThrown) {
+ $.pnotify({title: "Login failed", text: "User unknown or wrong password", type: "error"});
+ }
+ })
+ }
+
+ self.logout = function() {
+ $.ajax({
+ url: AJAX_BASEURL + "logout",
+ type: "POST",
+ success: function(response) {
+ $.pnotify({title: "Logout successful", text: "You are now logged out", type: "success"});
+ self.fromResponse(response);
+ }
+ })
+ }
+}
diff --git a/octoprint/static/js/app/viewmodels/navigation.js b/octoprint/static/js/app/viewmodels/navigation.js
new file mode 100644
index 0000000..c12ce0e
--- /dev/null
+++ b/octoprint/static/js/app/viewmodels/navigation.js
@@ -0,0 +1,33 @@
+function NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsViewModel, usersViewModel) {
+ var self = this;
+
+ self.loginState = loginStateViewModel;
+ self.appearance = appearanceViewModel;
+ self.systemActions = settingsViewModel.system_actions;
+ self.users = usersViewModel;
+
+ self.triggerAction = function(action) {
+ var callback = function() {
+ $.ajax({
+ url: AJAX_BASEURL + "system",
+ type: "POST",
+ dataType: "json",
+ data: "action=" + action.action,
+ success: function() {
+ $.pnotify({title: "Success", text: "The command \""+ action.name +"\" executed successfully", type: "success"});
+ },
+ error: function(jqXHR, textStatus, errorThrown) {
+ $.pnotify({title: "Error", text: "The command \"" + action.name + "\" could not be executed.
Reason:
" + jqXHR.responseText + "
", type: "error"});
+ }
+ })
+ }
+ if (action.confirm) {
+ $("#confirmation_dialog .confirmation_dialog_message").text(action.confirm);
+ $("#confirmation_dialog .confirmation_dialog_acknowledge").click(function(e) {e.preventDefault(); $("#confirmation_dialog").modal("hide"); callback(); });
+ $("#confirmation_dialog").modal("show");
+ } else {
+ callback();
+ }
+ }
+}
+
diff --git a/octoprint/static/js/app/viewmodels/printerstate.js b/octoprint/static/js/app/viewmodels/printerstate.js
new file mode 100644
index 0000000..6779d15
--- /dev/null
+++ b/octoprint/static/js/app/viewmodels/printerstate.js
@@ -0,0 +1,153 @@
+function PrinterStateViewModel(loginStateViewModel) {
+ var self = this;
+
+ self.loginState = loginStateViewModel;
+
+ self.stateString = ko.observable(undefined);
+ self.isErrorOrClosed = ko.observable(undefined);
+ self.isOperational = ko.observable(undefined);
+ self.isPrinting = ko.observable(undefined);
+ self.isPaused = ko.observable(undefined);
+ self.isError = ko.observable(undefined);
+ self.isReady = ko.observable(undefined);
+ self.isLoading = ko.observable(undefined);
+ self.isSdReady = ko.observable(undefined);
+
+ self.filename = 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.sd = ko.observable(undefined);
+ self.timelapse = ko.observable(undefined);
+
+ self.filament = ko.observable(undefined);
+ self.estimatedPrintTime = ko.observable(undefined);
+
+ self.currentHeight = ko.observable(undefined);
+
+ self.byteString = ko.computed(function() {
+ if (!self.filesize())
+ return "-";
+ 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;
+ return self.progress();
+ });
+ self.pauseString = ko.computed(function() {
+ if (self.isPaused())
+ return "Continue";
+ else
+ return "Pause";
+ });
+
+ self.timelapseString = ko.computed(function() {
+ var timelapse = self.timelapse();
+
+ if (!timelapse || !timelapse.hasOwnProperty("type"))
+ return "-";
+
+ var type = timelapse["type"];
+ if (type == "zchange") {
+ return "On Z Change";
+ } else if (type == "timed") {
+ return "Timed (" + timelapse["options"]["interval"] + "s)";
+ } else {
+ return "-";
+ }
+ });
+
+ self.fromCurrentData = function(data) {
+ self._fromData(data);
+ }
+
+ self.fromHistoryData = function(data) {
+ self._fromData(data);
+ }
+
+ self.fromTimelapseData = function(data) {
+ self.timelapse(data);
+ }
+
+ self._fromData = function(data) {
+ self._processStateData(data.state)
+ self._processJobData(data.job);
+ self._processProgressData(data.progress);
+ self._processZData(data.currentZ);
+ }
+
+ self._processStateData = function(data) {
+ self.stateString(data.stateString);
+ self.isErrorOrClosed(data.flags.closedOrError);
+ self.isOperational(data.flags.operational);
+ self.isPaused(data.flags.paused);
+ self.isPrinting(data.flags.printing);
+ self.isError(data.flags.error);
+ self.isReady(data.flags.ready);
+ self.isSdReady(data.flags.sdReady);
+ }
+
+ self._processJobData = function(data) {
+ self.filename(data.filename);
+ self.filesize(data.filesize);
+ self.estimatedPrintTime(data.estimatedPrintTime);
+ self.filament(data.filament);
+ self.sd(data.sd);
+ }
+
+ self._processProgressData = function(data) {
+ if (data.progress) {
+ self.progress(Math.round(data.progress * 100));
+ } else {
+ self.progress(undefined);
+ }
+ self.filepos(data.filepos);
+ self.printTime(data.printTime);
+ self.printTimeLeft(data.printTimeLeft);
+ }
+
+ self._processZData = function(data) {
+ self.currentHeight(data);
+ }
+
+ self.print = function() {
+ var printAction = function() {
+ self._jobCommand("start");
+ }
+
+ if (self.isPaused()) {
+ $("#confirmation_dialog .confirmation_dialog_message").text("This will restart the print job from the beginning.");
+ $("#confirmation_dialog .confirmation_dialog_acknowledge").click(function(e) {e.preventDefault(); $("#confirmation_dialog").modal("hide"); printAction(); });
+ $("#confirmation_dialog").modal("show");
+ } else {
+ printAction();
+ }
+
+ }
+
+ self.pause = function() {
+ self._jobCommand("pause");
+ }
+
+ self.cancel = function() {
+ self._jobCommand("cancel");
+ }
+
+ self._jobCommand = function(command) {
+ $.ajax({
+ url: AJAX_BASEURL + "control/job",
+ type: "POST",
+ dataType: "json",
+ data: {command: command}
+ });
+ }
+}
diff --git a/octoprint/static/js/app/viewmodels/settings.js b/octoprint/static/js/app/viewmodels/settings.js
new file mode 100644
index 0000000..c8cdd21
--- /dev/null
+++ b/octoprint/static/js/app/viewmodels/settings.js
@@ -0,0 +1,201 @@
+function SettingsViewModel(loginStateViewModel, usersViewModel) {
+ var self = this;
+
+ self.loginState = loginStateViewModel;
+ self.users = usersViewModel;
+
+ self.api_enabled = ko.observable(undefined);
+ self.api_key = ko.observable(undefined);
+
+ self.appearance_name = ko.observable(undefined);
+ self.appearance_color = ko.observable(undefined);
+
+ /* I did attempt to allow arbitrary gradients but cross browser support via knockout or jquery was going to be horrible */
+ self.appearance_available_colors = ko.observable(["default", "red", "orange", "yellow", "green", "blue", "violet", "black"]);
+
+ self.printer_movementSpeedX = ko.observable(undefined);
+ self.printer_movementSpeedY = ko.observable(undefined);
+ self.printer_movementSpeedZ = ko.observable(undefined);
+ self.printer_movementSpeedE = ko.observable(undefined);
+
+ self.webcam_streamUrl = ko.observable(undefined);
+ self.webcam_snapshotUrl = ko.observable(undefined);
+ self.webcam_ffmpegPath = ko.observable(undefined);
+ self.webcam_bitrate = ko.observable(undefined);
+ self.webcam_watermark = ko.observable(undefined);
+ self.webcam_flipH = ko.observable(undefined);
+ self.webcam_flipV = ko.observable(undefined);
+
+ self.feature_gcodeViewer = ko.observable(undefined);
+ self.feature_temperatureGraph = ko.observable(undefined);
+ self.feature_waitForStart = ko.observable(undefined);
+ self.feature_alwaysSendChecksum = ko.observable(undefined);
+ self.feature_sdSupport = ko.observable(undefined);
+ self.feature_swallowOkAfterResend = ko.observable(undefined);
+
+ self.serial_port = ko.observable();
+ self.serial_baudrate = ko.observable();
+ self.serial_portOptions = ko.observableArray([]);
+ self.serial_baudrateOptions = ko.observableArray([]);
+ self.serial_autoconnect = ko.observable(undefined);
+ self.serial_timeoutConnection = ko.observable(undefined);
+ self.serial_timeoutDetection = ko.observable(undefined);
+ self.serial_timeoutCommunication = ko.observable(undefined);
+ self.serial_log = ko.observable(undefined);
+
+ self.folder_uploads = ko.observable(undefined);
+ self.folder_timelapse = ko.observable(undefined);
+ self.folder_timelapseTmp = ko.observable(undefined);
+ self.folder_logs = ko.observable(undefined);
+
+ self.temperature_profiles = ko.observableArray(undefined);
+
+ self.system_actions = ko.observableArray([]);
+
+ self.terminalFilters = ko.observableArray([]);
+
+ self.addTemperatureProfile = function() {
+ self.temperature_profiles.push({name: "New", extruder:0, bed:0});
+ };
+
+ self.removeTemperatureProfile = function(profile) {
+ self.temperature_profiles.remove(profile);
+ };
+
+ self.addTerminalFilter = function() {
+ self.terminalFilters.push({name: "New", regex: "(Send: M105)|(Recv: ok T:)"})
+ };
+
+ self.removeTerminalFilter = function(filter) {
+ self.terminalFilters.remove(filter);
+ };
+
+ self.requestData = function() {
+ $.ajax({
+ url: AJAX_BASEURL + "settings",
+ type: "GET",
+ dataType: "json",
+ success: self.fromResponse
+ });
+ }
+
+ self.fromResponse = function(response) {
+ self.api_enabled(response.api.enabled);
+ self.api_key(response.api.key);
+
+ self.appearance_name(response.appearance.name);
+ self.appearance_color(response.appearance.color);
+
+ self.printer_movementSpeedX(response.printer.movementSpeedX);
+ self.printer_movementSpeedY(response.printer.movementSpeedY);
+ self.printer_movementSpeedZ(response.printer.movementSpeedZ);
+ self.printer_movementSpeedE(response.printer.movementSpeedE);
+
+ self.webcam_streamUrl(response.webcam.streamUrl);
+ self.webcam_snapshotUrl(response.webcam.snapshotUrl);
+ self.webcam_ffmpegPath(response.webcam.ffmpegPath);
+ self.webcam_bitrate(response.webcam.bitrate);
+ self.webcam_watermark(response.webcam.watermark);
+ self.webcam_flipH(response.webcam.flipH);
+ self.webcam_flipV(response.webcam.flipV);
+
+ self.feature_gcodeViewer(response.feature.gcodeViewer);
+ self.feature_temperatureGraph(response.feature.temperatureGraph);
+ self.feature_waitForStart(response.feature.waitForStart);
+ self.feature_alwaysSendChecksum(response.feature.alwaysSendChecksum);
+ self.feature_sdSupport(response.feature.sdSupport);
+ self.feature_swallowOkAfterResend(response.feature.swallowOkAfterResend);
+
+ self.serial_port(response.serial.port);
+ self.serial_baudrate(response.serial.baudrate);
+ self.serial_portOptions(response.serial.portOptions);
+ self.serial_baudrateOptions(response.serial.baudrateOptions);
+ self.serial_autoconnect(response.serial.autoconnect);
+ self.serial_timeoutConnection(response.serial.timeoutConnection);
+ self.serial_timeoutDetection(response.serial.timeoutDetection);
+ self.serial_timeoutCommunication(response.serial.timeoutCommunication);
+ self.serial_log(response.serial.log);
+
+ self.folder_uploads(response.folder.uploads);
+ self.folder_timelapse(response.folder.timelapse);
+ self.folder_timelapseTmp(response.folder.timelapseTmp);
+ self.folder_logs(response.folder.logs);
+
+ self.temperature_profiles(response.temperature.profiles);
+
+ self.system_actions(response.system.actions);
+
+ self.terminalFilters(response.terminalFilters);
+ }
+
+ self.saveData = function() {
+ var data = {
+ "api" : {
+ "enabled": self.api_enabled(),
+ "key": self.api_key()
+ },
+ "appearance" : {
+ "name": self.appearance_name(),
+ "color": self.appearance_color()
+ },
+ "printer": {
+ "movementSpeedX": self.printer_movementSpeedX(),
+ "movementSpeedY": self.printer_movementSpeedY(),
+ "movementSpeedZ": self.printer_movementSpeedZ(),
+ "movementSpeedE": self.printer_movementSpeedE()
+ },
+ "webcam": {
+ "streamUrl": self.webcam_streamUrl(),
+ "snapshotUrl": self.webcam_snapshotUrl(),
+ "ffmpegPath": self.webcam_ffmpegPath(),
+ "bitrate": self.webcam_bitrate(),
+ "watermark": self.webcam_watermark(),
+ "flipH": self.webcam_flipH(),
+ "flipV": self.webcam_flipV()
+ },
+ "feature": {
+ "gcodeViewer": self.feature_gcodeViewer(),
+ "temperatureGraph": self.feature_temperatureGraph(),
+ "waitForStart": self.feature_waitForStart(),
+ "alwaysSendChecksum": self.feature_alwaysSendChecksum(),
+ "sdSupport": self.feature_sdSupport(),
+ "swallowOkAfterResend": self.feature_swallowOkAfterResend()
+ },
+ "serial": {
+ "port": self.serial_port(),
+ "baudrate": self.serial_baudrate(),
+ "autoconnect": self.serial_autoconnect(),
+ "timeoutConnection": self.serial_timeoutConnection(),
+ "timeoutDetection": self.serial_timeoutDetection(),
+ "timeoutCommunication": self.serial_timeoutCommunication(),
+ "log": self.serial_log()
+ },
+ "folder": {
+ "uploads": self.folder_uploads(),
+ "timelapse": self.folder_timelapse(),
+ "timelapseTmp": self.folder_timelapseTmp(),
+ "logs": self.folder_logs()
+ },
+ "temperature": {
+ "profiles": self.temperature_profiles()
+ },
+ "system": {
+ "actions": self.system_actions()
+ },
+ "terminalFilters": self.terminalFilters()
+ }
+
+ $.ajax({
+ url: AJAX_BASEURL + "settings",
+ type: "POST",
+ dataType: "json",
+ contentType: "application/json; charset=UTF-8",
+ data: JSON.stringify(data),
+ success: function(response) {
+ self.fromResponse(response);
+ $("#settings_dialog").modal("hide");
+ }
+ })
+ }
+
+}
diff --git a/octoprint/static/js/app/viewmodels/temperature.js b/octoprint/static/js/app/viewmodels/temperature.js
new file mode 100644
index 0000000..6890914
--- /dev/null
+++ b/octoprint/static/js/app/viewmodels/temperature.js
@@ -0,0 +1,222 @@
+function TemperatureViewModel(loginStateViewModel, settingsViewModel) {
+ var self = this;
+
+ self.loginState = loginStateViewModel;
+
+ self.temp = ko.observable(undefined);
+ self.bedTemp = ko.observable(undefined);
+ self.targetTemp = ko.observable(undefined);
+ self.bedTargetTemp = ko.observable(undefined);
+
+ self.newTemp = ko.observable(undefined);
+ self.newBedTemp = ko.observable(undefined);
+
+ self.newTempOffset = ko.observable(undefined);
+ self.tempOffset = ko.observable(0);
+ self.newBedTempOffset = ko.observable(undefined);
+ self.bedTempOffset = ko.observable(0);
+
+ self.isErrorOrClosed = ko.observable(undefined);
+ self.isOperational = ko.observable(undefined);
+ self.isPrinting = ko.observable(undefined);
+ self.isPaused = ko.observable(undefined);
+ self.isError = ko.observable(undefined);
+ self.isReady = ko.observable(undefined);
+ self.isLoading = ko.observable(undefined);
+
+ self.temperature_profiles = settingsViewModel.temperature_profiles;
+
+ self.tempString = ko.computed(function() {
+ if (!self.temp())
+ return "-";
+ return self.temp() + " °C";
+ });
+ self.bedTempString = ko.computed(function() {
+ if (!self.bedTemp())
+ return "-";
+ return self.bedTemp() + " °C";
+ });
+ self.targetTempString = ko.computed(function() {
+ if (!self.targetTemp())
+ return "-";
+ return self.targetTemp() + " °C";
+ });
+ self.bedTargetTempString = ko.computed(function() {
+ if (!self.bedTargetTemp())
+ return "-";
+ return self.bedTargetTemp() + " °C";
+ });
+
+ self.temperatures = [];
+ self.plotOptions = {
+ yaxis: {
+ min: 0,
+ max: 310,
+ ticks: 10
+ },
+ xaxis: {
+ mode: "time",
+ minTickSize: [2, "minute"],
+ tickFormatter: function(val, axis) {
+ if (val == undefined || val == 0)
+ return ""; // we don't want to display the minutes since the epoch if not connected yet ;)
+
+ // current time in milliseconds in UTC
+ var timestampUtc = Date.now();
+
+ // calculate difference in milliseconds
+ var diff = timestampUtc - val;
+
+ // convert to minutes
+ var diffInMins = Math.round(diff / (60 * 1000));
+ if (diffInMins == 0)
+ return "just now";
+ else
+ return "- " + diffInMins + " min";
+ }
+ },
+ legend: {
+ noColumns: 4
+ }
+ }
+
+ self.fromCurrentData = function(data) {
+ self._processStateData(data.state);
+ self._processTemperatureUpdateData(data.temperatures);
+ self._processOffsetData(data.offsets);
+ }
+
+ self.fromHistoryData = function(data) {
+ self._processStateData(data.state);
+ self._processTemperatureHistoryData(data.temperatureHistory);
+ self._processOffsetData(data.offsets);
+ }
+
+ self._processStateData = function(data) {
+ self.isErrorOrClosed(data.flags.closedOrError);
+ self.isOperational(data.flags.operational);
+ self.isPaused(data.flags.paused);
+ self.isPrinting(data.flags.printing);
+ self.isError(data.flags.error);
+ self.isReady(data.flags.ready);
+ self.isLoading(data.flags.loading);
+ }
+
+ self._processTemperatureUpdateData = function(data) {
+ if (data.length == 0)
+ return;
+
+ self.temp(data[data.length - 1].temp);
+ self.bedTemp(data[data.length - 1].bedTemp);
+ self.targetTemp(data[data.length - 1].targetTemp);
+ self.bedTargetTemp(data[data.length - 1].targetBedTemp);
+
+ if (!CONFIG_TEMPERATURE_GRAPH) return;
+
+ if (!self.temperatures)
+ self.temperatures = [];
+ if (!self.temperatures.actual)
+ self.temperatures.actual = [];
+ if (!self.temperatures.target)
+ self.temperatures.target = [];
+ if (!self.temperatures.actualBed)
+ self.temperatures.actualBed = [];
+ if (!self.temperatures.targetBed)
+ self.temperatures.targetBed = [];
+
+ _.each(data, function(d) {
+ var time = d.currentTime;
+ self.temperatures.actual.push([time, d.temp]);
+ self.temperatures.target.push([time, d.targetTemp]);
+ self.temperatures.actualBed.push([time, d.bedTemp]);
+ self.temperatures.targetBed.push([time, d.targetBedTemp]);
+ });
+
+ self.temperatures.actual = self.temperatures.actual.slice(-300);
+ self.temperatures.target = self.temperatures.target.slice(-300);
+ self.temperatures.actualBed = self.temperatures.actualBed.slice(-300);
+ self.temperatures.targetBed = self.temperatures.targetBed.slice(-300);
+
+ self.updatePlot();
+ }
+
+ self._processTemperatureHistoryData = function(data) {
+ self.temperatures = data;
+ self.updatePlot();
+ }
+
+ self._processOffsetData = function(data) {
+ self.tempOffset(data[0]);
+ self.bedTempOffset(data[1]);
+ }
+
+ self.updatePlot = function() {
+ var graph = $("#temperature-graph");
+ if (graph.length) {
+ var data = [
+ {label: "Actual", color: "#FF4040", data: self.temperatures.actual},
+ {label: "Target", color: "#FFA0A0", data: self.temperatures.target},
+ {label: "Bed Actual", color: "#4040FF", data: self.temperatures.actualBed},
+ {label: "Bed Target", color: "#A0A0FF", data: self.temperatures.targetBed}
+ ]
+
+ $.plot(graph, data, self.plotOptions);
+ }
+ }
+
+ self.setTempFromProfile = function(profile) {
+ if (!profile)
+ return;
+ self._updateTemperature(profile.extruder, "temp");
+ }
+
+ self.setTemp = function() {
+ self._updateTemperature(self.newTemp(), "temp", function(){self.targetTemp(self.newTemp()); self.newTemp("");});
+ };
+
+ self.setTempToZero = function() {
+ self._updateTemperature(0, "temp", function(){self.targetTemp(0); self.newTemp("");});
+ }
+
+ self.setTempOffset = function() {
+ self._updateTemperature(self.newTempOffset(), "tempOffset", function() {self.tempOffset(self.newTempOffset()); self.newTempOffset("");});
+ }
+
+ self.setBedTempFromProfile = function(profile) {
+ self._updateTemperature(profile.bed, "bedTemp");
+ }
+
+ self.setBedTemp = function() {
+ self._updateTemperature(self.newBedTemp(), "bedTemp", function() {self.bedTargetTemp(self.newBedTemp()); self.newBedTemp("");});
+ };
+
+ self.setBedTempToZero = function() {
+ self._updateTemperature(0, "bedTemp", function() {self.bedTargetTemp(0); self.newBedTemp("");});
+ }
+
+ self.setBedTempOffset = function() {
+ self._updateTemperature(self.newBedTempOffset(), "bedTempOffset", function() {self.bedTempOffset(self.newBedTempOffset()); self.newBedTempOffset("");});
+ }
+
+ self._updateTemperature = function(temp, type, callback) {
+ var data = {};
+ data[type] = temp;
+
+ $.ajax({
+ url: AJAX_BASEURL + "control/temperature",
+ type: "POST",
+ data: data,
+ success: function() { if (callback !== undefined) callback(); }
+ });
+ }
+
+ self.handleEnter = function(event, type) {
+ if (event.keyCode == 13) {
+ if (type == "temp") {
+ self.setTemp();
+ } else if (type == "bedTemp") {
+ self.setBedTemp();
+ }
+ }
+ }
+}
diff --git a/octoprint/static/js/app/viewmodels/terminal.js b/octoprint/static/js/app/viewmodels/terminal.js
new file mode 100644
index 0000000..01eab23
--- /dev/null
+++ b/octoprint/static/js/app/viewmodels/terminal.js
@@ -0,0 +1,119 @@
+function TerminalViewModel(loginStateViewModel, settingsViewModel) {
+ var self = this;
+
+ self.loginState = loginStateViewModel;
+ self.settings = settingsViewModel;
+
+ self.log = [];
+
+ self.command = ko.observable(undefined);
+
+ self.isErrorOrClosed = ko.observable(undefined);
+ self.isOperational = ko.observable(undefined);
+ self.isPrinting = ko.observable(undefined);
+ self.isPaused = ko.observable(undefined);
+ self.isError = ko.observable(undefined);
+ self.isReady = ko.observable(undefined);
+ self.isLoading = ko.observable(undefined);
+
+ self.autoscrollEnabled = ko.observable(true);
+
+ self.filters = self.settings.terminalFilters;
+ self.filterRegex = undefined;
+
+ self.activeFilters = ko.observableArray([]);
+ self.activeFilters.subscribe(function(e) {
+ self.updateFilterRegex();
+ self.updateOutput();
+ });
+
+ self.fromCurrentData = function(data) {
+ self._processStateData(data.state);
+ self._processCurrentLogData(data.logs);
+ }
+
+ self.fromHistoryData = function(data) {
+ self._processStateData(data.state);
+ self._processHistoryLogData(data.logHistory);
+ }
+
+ self._processCurrentLogData = function(data) {
+ if (!self.log)
+ self.log = []
+ self.log = self.log.concat(data)
+ self.log = self.log.slice(-300)
+ self.updateOutput();
+ }
+
+ self._processHistoryLogData = function(data) {
+ self.log = data;
+ self.updateOutput();
+ }
+
+ self._processStateData = function(data) {
+ self.isErrorOrClosed(data.flags.closedOrError);
+ self.isOperational(data.flags.operational);
+ self.isPaused(data.flags.paused);
+ self.isPrinting(data.flags.printing);
+ self.isError(data.flags.error);
+ self.isReady(data.flags.ready);
+ self.isLoading(data.flags.loading);
+ }
+
+ self.updateFilterRegex = function() {
+ var filterRegexStr = self.activeFilters().join("|").trim();
+ if (filterRegexStr == "") {
+ self.filterRegex = undefined;
+ } else {
+ self.filterRegex = new RegExp(filterRegexStr);
+ }
+ console.log("Terminal filter regex: " + filterRegexStr);
+ }
+
+ self.updateOutput = function() {
+ if (!self.log)
+ return;
+
+ var output = "";
+ for (var i = 0; i < self.log.length; i++) {
+ if (self.filterRegex !== undefined && self.log[i].match(self.filterRegex)) continue;
+ output += self.log[i] + "\n";
+ }
+
+ var container = $("#terminal-output");
+ container.text(output);
+
+ if (self.autoscrollEnabled()) {
+ container.scrollTop(container[0].scrollHeight - container.height())
+ }
+ }
+
+ self.sendCommand = function() {
+ /*
+ var re = /^([gm][0-9]+)(\s.*)?/;
+ var commandMatch = command.match(re);
+ if (commandMatch != null) {
+ command = commandMatch[1].toUpperCase() + ((commandMatch[2] !== undefined) ? commandMatch[2] : "");
+ }
+ */
+
+ var command = self.command();
+ if (command) {
+ $.ajax({
+ url: AJAX_BASEURL + "control/command",
+ type: "POST",
+ dataType: "json",
+ contentType: "application/json; charset=UTF-8",
+ data: JSON.stringify({"command": command})
+ });
+ self.command("");
+ }
+ }
+
+ self.handleEnter = function(event) {
+ if (event.keyCode == 13) {
+ self.sendCommand();
+ }
+ }
+
+}
diff --git a/octoprint/static/js/app/viewmodels/timelapse.js b/octoprint/static/js/app/viewmodels/timelapse.js
new file mode 100644
index 0000000..a882df9
--- /dev/null
+++ b/octoprint/static/js/app/viewmodels/timelapse.js
@@ -0,0 +1,137 @@
+function TimelapseViewModel(loginStateViewModel) {
+ var self = this;
+
+ self.loginState = loginStateViewModel;
+
+ self.timelapseType = ko.observable(undefined);
+ self.timelapseTimedInterval = ko.observable(undefined);
+
+ self.persist = ko.observable(false);
+ self.isDirty = ko.observable(false);
+
+ self.isErrorOrClosed = ko.observable(undefined);
+ self.isOperational = ko.observable(undefined);
+ self.isPrinting = ko.observable(undefined);
+ self.isPaused = ko.observable(undefined);
+ self.isError = ko.observable(undefined);
+ self.isReady = ko.observable(undefined);
+ self.isLoading = ko.observable(undefined);
+
+ self.intervalInputEnabled = ko.computed(function() {
+ return ("timed" == self.timelapseType());
+ });
+ self.saveButtonEnabled = ko.computed(function() {
+ return self.isDirty() && self.isOperational() && !self.isPrinting() && self.loginState.isUser();
+ });
+
+ self.isOperational.subscribe(function(newValue) {
+ self.requestData();
+ });
+
+ self.timelapseType.subscribe(function(newValue) {
+ self.isDirty(true);
+ });
+ self.timelapseTimedInterval.subscribe(function(newValue) {
+ self.isDirty(true);
+ });
+
+ // initialize list helper
+ self.listHelper = new ItemListHelper(
+ "timelapseFiles",
+ {
+ "name": function(a, b) {
+ // sorts ascending
+ if (a["name"].toLocaleLowerCase() < b["name"].toLocaleLowerCase()) return -1;
+ if (a["name"].toLocaleLowerCase() > b["name"].toLocaleLowerCase()) return 1;
+ return 0;
+ },
+ "creation": function(a, b) {
+ // sorts descending
+ if (a["date"] > b["date"]) return -1;
+ if (a["date"] < b["date"]) return 1;
+ return 0;
+ },
+ "size": function(a, b) {
+ // sorts descending
+ if (a["bytes"] > b["bytes"]) return -1;
+ if (a["bytes"] < b["bytes"]) return 1;
+ return 0;
+ }
+ },
+ {
+ },
+ "name",
+ [],
+ [],
+ CONFIG_TIMELAPSEFILESPERPAGE
+ );
+
+ self.requestData = function() {
+ $.ajax({
+ url: AJAX_BASEURL + "timelapse",
+ type: "GET",
+ dataType: "json",
+ success: self.fromResponse
+ });
+ };
+
+ self.fromResponse = function(response) {
+ self.timelapseType(response.type);
+ self.listHelper.updateItems(response.files);
+
+ if (response.type == "timed" && response.config && response.config.interval) {
+ self.timelapseTimedInterval(response.config.interval);
+ } else {
+ self.timelapseTimedInterval(undefined);
+ }
+
+ self.persist(false);
+ self.isDirty(false);
+ }
+
+ self.fromCurrentData = function(data) {
+ self._processStateData(data.state);
+ }
+
+ self.fromHistoryData = function(data) {
+ self._processStateData(data.state);
+ }
+
+ self._processStateData = function(data) {
+ self.isErrorOrClosed(data.flags.closedOrError);
+ self.isOperational(data.flags.operational);
+ self.isPaused(data.flags.paused);
+ self.isPrinting(data.flags.printing);
+ self.isError(data.flags.error);
+ self.isReady(data.flags.ready);
+ self.isLoading(data.flags.loading);
+ }
+
+ self.removeFile = function(filename) {
+ $.ajax({
+ url: AJAX_BASEURL + "timelapse/" + filename,
+ type: "DELETE",
+ dataType: "json",
+ success: self.requestData
+ });
+ }
+
+ self.save = function(data, event) {
+ var data = {
+ "type": self.timelapseType(),
+ "save": self.persist()
+ }
+
+ if (self.timelapseType() == "timed") {
+ data["interval"] = self.timelapseTimedInterval();
+ }
+
+ $.ajax({
+ url: AJAX_BASEURL + "timelapse",
+ type: "POST",
+ dataType: "json",
+ data: data,
+ success: self.fromResponse
+ });
+ }
+}
diff --git a/octoprint/static/js/app/viewmodels/users.js b/octoprint/static/js/app/viewmodels/users.js
new file mode 100644
index 0000000..70c8534
--- /dev/null
+++ b/octoprint/static/js/app/viewmodels/users.js
@@ -0,0 +1,190 @@
+function UsersViewModel(loginStateViewModel) {
+ var self = this;
+
+ self.loginState = loginStateViewModel;
+
+ // initialize list helper
+ self.listHelper = new ItemListHelper(
+ "users",
+ {
+ "name": function(a, b) {
+ // sorts ascending
+ if (a["name"].toLocaleLowerCase() < b["name"].toLocaleLowerCase()) return -1;
+ if (a["name"].toLocaleLowerCase() > b["name"].toLocaleLowerCase()) return 1;
+ return 0;
+ }
+ },
+ {},
+ "name",
+ [],
+ [],
+ CONFIG_USERSPERPAGE
+ );
+
+ self.emptyUser = {name: "", admin: false, active: false};
+
+ self.currentUser = ko.observable(self.emptyUser);
+
+ self.editorUsername = ko.observable(undefined);
+ self.editorPassword = ko.observable(undefined);
+ self.editorRepeatedPassword = ko.observable(undefined);
+ self.editorAdmin = ko.observable(undefined);
+ self.editorActive = ko.observable(undefined);
+
+ self.currentUser.subscribe(function(newValue) {
+ if (newValue === undefined) {
+ self.editorUsername(undefined);
+ self.editorAdmin(undefined);
+ self.editorActive(undefined);
+ } else {
+ self.editorUsername(newValue.name);
+ self.editorAdmin(newValue.admin);
+ self.editorActive(newValue.active);
+ }
+ self.editorPassword(undefined);
+ self.editorRepeatedPassword(undefined);
+ });
+
+ self.editorPasswordMismatch = ko.computed(function() {
+ return self.editorPassword() != self.editorRepeatedPassword();
+ });
+
+ self.requestData = function() {
+ if (!CONFIG_ACCESS_CONTROL) return;
+
+ $.ajax({
+ url: AJAX_BASEURL + "users",
+ type: "GET",
+ dataType: "json",
+ success: self.fromResponse
+ });
+ }
+
+ self.fromResponse = function(response) {
+ self.listHelper.updateItems(response.users);
+ }
+
+ self.showAddUserDialog = function() {
+ if (!CONFIG_ACCESS_CONTROL) return;
+
+ self.currentUser(undefined);
+ self.editorActive(true);
+ $("#settings-usersDialogAddUser").modal("show");
+ }
+
+ self.confirmAddUser = function() {
+ if (!CONFIG_ACCESS_CONTROL) return;
+
+ var user = {name: self.editorUsername(), password: self.editorPassword(), admin: self.editorAdmin(), active: self.editorActive()};
+ self.addUser(user, function() {
+ // close dialog
+ self.currentUser(undefined);
+ $("#settings-usersDialogAddUser").modal("hide");
+ });
+ }
+
+ self.showEditUserDialog = function(user) {
+ if (!CONFIG_ACCESS_CONTROL) return;
+
+ self.currentUser(user);
+ $("#settings-usersDialogEditUser").modal("show");
+ }
+
+ self.confirmEditUser = function() {
+ if (!CONFIG_ACCESS_CONTROL) return;
+
+ var user = self.currentUser();
+ user.active = self.editorActive();
+ user.admin = self.editorAdmin();
+
+ // make AJAX call
+ self.updateUser(user, function() {
+ // close dialog
+ self.currentUser(undefined);
+ $("#settings-usersDialogEditUser").modal("hide");
+ });
+ }
+
+ self.showChangePasswordDialog = function(user) {
+ if (!CONFIG_ACCESS_CONTROL) return;
+
+ self.currentUser(user);
+ $("#settings-usersDialogChangePassword").modal("show");
+ }
+
+ self.confirmChangePassword = function() {
+ if (!CONFIG_ACCESS_CONTROL) return;
+
+ self.updatePassword(self.currentUser().name, self.editorPassword(), function() {
+ // close dialog
+ self.currentUser(undefined);
+ $("#settings-usersDialogChangePassword").modal("hide");
+ });
+ }
+
+ //~~ AJAX calls
+
+ self.addUser = function(user, callback) {
+ if (!CONFIG_ACCESS_CONTROL) return;
+ if (user === undefined) return;
+
+ $.ajax({
+ url: AJAX_BASEURL + "users",
+ type: "POST",
+ contentType: "application/json; charset=UTF-8",
+ data: JSON.stringify(user),
+ success: function(response) {
+ self.fromResponse(response);
+ callback();
+ }
+ });
+ }
+
+ self.removeUser = function(user, callback) {
+ if (!CONFIG_ACCESS_CONTROL) return;
+ if (user === undefined) return;
+
+ if (user.name == loginStateViewModel.username()) {
+ // we do not allow to delete ourselves
+ $.pnotify({title: "Not possible", text: "You may not delete your own account.", type: "error"});
+ return;
+ }
+
+ $.ajax({
+ url: AJAX_BASEURL + "users/" + user.name,
+ type: "DELETE",
+ success: function(response) {
+ self.fromResponse(response);
+ callback();
+ }
+ });
+ }
+
+ self.updateUser = function(user, callback) {
+ if (!CONFIG_ACCESS_CONTROL) return;
+ if (user === undefined) return;
+
+ $.ajax({
+ url: AJAX_BASEURL + "users/" + user.name,
+ type: "PUT",
+ contentType: "application/json; charset=UTF-8",
+ data: JSON.stringify(user),
+ success: function(response) {
+ self.fromResponse(response);
+ callback();
+ }
+ });
+ }
+
+ self.updatePassword = function(username, password, callback) {
+ if (!CONFIG_ACCESS_CONTROL) return;
+
+ $.ajax({
+ url: AJAX_BASEURL + "users/" + username + "/password",
+ type: "PUT",
+ contentType: "application/json; charset=UTF-8",
+ data: JSON.stringify({password: password}),
+ success: callback
+ });
+ }
+}
diff --git a/octoprint/static/js/lib/avltree.js b/octoprint/static/js/lib/avltree.js
new file mode 100644
index 0000000..b329de1
--- /dev/null
+++ b/octoprint/static/js/lib/avltree.js
@@ -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;
+ }
+}
diff --git a/octoprint/static/js/bootstrap/bootstrap-modal.js b/octoprint/static/js/lib/bootstrap/bootstrap-modal.js
similarity index 100%
rename from octoprint/static/js/bootstrap/bootstrap-modal.js
rename to octoprint/static/js/lib/bootstrap/bootstrap-modal.js
diff --git a/octoprint/static/js/bootstrap/bootstrap-modalmanager.js b/octoprint/static/js/lib/bootstrap/bootstrap-modalmanager.js
similarity index 100%
rename from octoprint/static/js/bootstrap/bootstrap-modalmanager.js
rename to octoprint/static/js/lib/bootstrap/bootstrap-modalmanager.js
diff --git a/octoprint/static/js/bootstrap/bootstrap.js b/octoprint/static/js/lib/bootstrap/bootstrap.js
similarity index 100%
rename from octoprint/static/js/bootstrap/bootstrap.js
rename to octoprint/static/js/lib/bootstrap/bootstrap.js
diff --git a/octoprint/static/js/bootstrap/bootstrap.min.js b/octoprint/static/js/lib/bootstrap/bootstrap.min.js
similarity index 100%
rename from octoprint/static/js/bootstrap/bootstrap.min.js
rename to octoprint/static/js/lib/bootstrap/bootstrap.min.js
diff --git a/octoprint/static/js/jquery/jquery.fileupload.js b/octoprint/static/js/lib/jquery/jquery.fileupload.js
similarity index 100%
rename from octoprint/static/js/jquery/jquery.fileupload.js
rename to octoprint/static/js/lib/jquery/jquery.fileupload.js
diff --git a/octoprint/static/js/jquery/jquery.flot.js b/octoprint/static/js/lib/jquery/jquery.flot.js
similarity index 100%
rename from octoprint/static/js/jquery/jquery.flot.js
rename to octoprint/static/js/lib/jquery/jquery.flot.js
diff --git a/octoprint/static/js/jquery/jquery.iframe-transport.js b/octoprint/static/js/lib/jquery/jquery.iframe-transport.js
similarity index 100%
rename from octoprint/static/js/jquery/jquery.iframe-transport.js
rename to octoprint/static/js/lib/jquery/jquery.iframe-transport.js
diff --git a/octoprint/static/js/lib/jquery/jquery.min.js b/octoprint/static/js/lib/jquery/jquery.min.js
new file mode 100644
index 0000000..b18e05a
--- /dev/null
+++ b/octoprint/static/js/lib/jquery/jquery.min.js
@@ -0,0 +1,6 @@
+/*! jQuery v2.0.0 | (c) 2005, 2013 jQuery Foundation, Inc. | jquery.org/license
+//@ sourceMappingURL=jquery.min.map
+*/
+(function(e,undefined){var t,n,r=typeof undefined,i=e.location,o=e.document,s=o.documentElement,a=e.jQuery,u=e.$,l={},c=[],f="2.0.0",p=c.concat,h=c.push,d=c.slice,g=c.indexOf,m=l.toString,y=l.hasOwnProperty,v=f.trim,x=function(e,n){return new x.fn.init(e,n,t)},b=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,w=/\S+/g,T=/^(?:(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,k=/^-ms-/,N=/-([\da-z])/gi,E=function(e,t){return t.toUpperCase()},S=function(){o.removeEventListener("DOMContentLoaded",S,!1),e.removeEventListener("load",S,!1),x.ready()};x.fn=x.prototype={jquery:f,constructor:x,init:function(e,t,n){var r,i;if(!e)return this;if("string"==typeof e){if(r="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:T.exec(e),!r||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof x?t[0]:t,x.merge(this,x.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:o,!0)),C.test(r[1])&&x.isPlainObject(t))for(r in t)x.isFunction(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return i=o.getElementById(r[2]),i&&i.parentNode&&(this.length=1,this[0]=i),this.context=o,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):x.isFunction(e)?n.ready(e):(e.selector!==undefined&&(this.selector=e.selector,this.context=e.context),x.makeArray(e,this))},selector:"",length:0,toArray:function(){return d.call(this)},get:function(e){return null==e?this.toArray():0>e?this[this.length+e]:this[e]},pushStack:function(e){var t=x.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return x.each(this,e,t)},ready:function(e){return x.ready.promise().done(e),this},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},map:function(e){return this.pushStack(x.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:h,sort:[].sort,splice:[].splice},x.fn.init.prototype=x.fn,x.extend=x.fn.extend=function(){var e,t,n,r,i,o,s=arguments[0]||{},a=1,u=arguments.length,l=!1;for("boolean"==typeof s&&(l=s,s=arguments[1]||{},a=2),"object"==typeof s||x.isFunction(s)||(s={}),u===a&&(s=this,--a);u>a;a++)if(null!=(e=arguments[a]))for(t in e)n=s[t],r=e[t],s!==r&&(l&&r&&(x.isPlainObject(r)||(i=x.isArray(r)))?(i?(i=!1,o=n&&x.isArray(n)?n:[]):o=n&&x.isPlainObject(n)?n:{},s[t]=x.extend(l,o,r)):r!==undefined&&(s[t]=r));return s},x.extend({expando:"jQuery"+(f+Math.random()).replace(/\D/g,""),noConflict:function(t){return e.$===x&&(e.$=u),t&&e.jQuery===x&&(e.jQuery=a),x},isReady:!1,readyWait:1,holdReady:function(e){e?x.readyWait++:x.ready(!0)},ready:function(e){(e===!0?--x.readyWait:x.isReady)||(x.isReady=!0,e!==!0&&--x.readyWait>0||(n.resolveWith(o,[x]),x.fn.trigger&&x(o).trigger("ready").off("ready")))},isFunction:function(e){return"function"===x.type(e)},isArray:Array.isArray,isWindow:function(e){return null!=e&&e===e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[m.call(e)]||"object":typeof e},isPlainObject:function(e){if("object"!==x.type(e)||e.nodeType||x.isWindow(e))return!1;try{if(e.constructor&&!y.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(t){return!1}return!0},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw Error(e)},parseHTML:function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||o;var r=C.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=x.buildFragment([e],t,i),i&&x(i).remove(),x.merge([],r.childNodes))},parseJSON:JSON.parse,parseXML:function(e){var t,n;if(!e||"string"!=typeof e)return null;try{n=new DOMParser,t=n.parseFromString(e,"text/xml")}catch(r){t=undefined}return(!t||t.getElementsByTagName("parsererror").length)&&x.error("Invalid XML: "+e),t},noop:function(){},globalEval:function(e){var t,n=eval;e=x.trim(e),e&&(1===e.indexOf("use strict")?(t=o.createElement("script"),t.text=e,o.head.appendChild(t).parentNode.removeChild(t)):n(e))},camelCase:function(e){return e.replace(k,"ms-").replace(N,E)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,n){var r,i=0,o=e.length,s=j(e);if(n){if(s){for(;o>i;i++)if(r=t.apply(e[i],n),r===!1)break}else for(i in e)if(r=t.apply(e[i],n),r===!1)break}else if(s){for(;o>i;i++)if(r=t.call(e[i],i,e[i]),r===!1)break}else for(i in e)if(r=t.call(e[i],i,e[i]),r===!1)break;return e},trim:function(e){return null==e?"":v.call(e)},makeArray:function(e,t){var n=t||[];return null!=e&&(j(Object(e))?x.merge(n,"string"==typeof e?[e]:e):h.call(n,e)),n},inArray:function(e,t,n){return null==t?-1:g.call(t,e,n)},merge:function(e,t){var n=t.length,r=e.length,i=0;if("number"==typeof n)for(;n>i;i++)e[r++]=t[i];else while(t[i]!==undefined)e[r++]=t[i++];return e.length=r,e},grep:function(e,t,n){var r,i=[],o=0,s=e.length;for(n=!!n;s>o;o++)r=!!t(e[o],o),n!==r&&i.push(e[o]);return i},map:function(e,t,n){var r,i=0,o=e.length,s=j(e),a=[];if(s)for(;o>i;i++)r=t(e[i],i,n),null!=r&&(a[a.length]=r);else for(i in e)r=t(e[i],i,n),null!=r&&(a[a.length]=r);return p.apply([],a)},guid:1,proxy:function(e,t){var n,r,i;return"string"==typeof t&&(n=e[t],t=e,e=n),x.isFunction(e)?(r=d.call(arguments,2),i=function(){return e.apply(t||this,r.concat(d.call(arguments)))},i.guid=e.guid=e.guid||x.guid++,i):undefined},access:function(e,t,n,r,i,o,s){var a=0,u=e.length,l=null==n;if("object"===x.type(n)){i=!0;for(a in n)x.access(e,t,a,n[a],!0,o,s)}else if(r!==undefined&&(i=!0,x.isFunction(r)||(s=!0),l&&(s?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(x(e),n)})),t))for(;u>a;a++)t(e[a],n,s?r:r.call(e[a],a,t(e[a],n)));return i?e:l?t.call(e):u?t(e[0],n):o},now:Date.now,swap:function(e,t,n,r){var i,o,s={};for(o in t)s[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=s[o];return i}}),x.ready.promise=function(t){return n||(n=x.Deferred(),"complete"===o.readyState?setTimeout(x.ready):(o.addEventListener("DOMContentLoaded",S,!1),e.addEventListener("load",S,!1))),n.promise(t)},x.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){l["[object "+t+"]"]=t.toLowerCase()});function j(e){var t=e.length,n=x.type(e);return x.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}t=x(o),function(e,undefined){var t,n,r,i,o,s,a,u,l,c,f,p,h,d,g,m,y="sizzle"+-new Date,v=e.document,b={},w=0,T=0,C=ot(),k=ot(),N=ot(),E=!1,S=function(){return 0},j=typeof undefined,D=1<<31,A=[],L=A.pop,q=A.push,H=A.push,O=A.slice,F=A.indexOf||function(e){var t=0,n=this.length;for(;n>t;t++)if(this[t]===e)return t;return-1},P="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",R="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",W=M.replace("w","w#"),$="\\["+R+"*("+M+")"+R+"*(?:([*^$|!~]?=)"+R+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+W+")|)|)"+R+"*\\]",B=":("+M+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+$.replace(3,8)+")*)|.*)\\)|)",I=RegExp("^"+R+"+|((?:^|[^\\\\])(?:\\\\.)*)"+R+"+$","g"),z=RegExp("^"+R+"*,"+R+"*"),_=RegExp("^"+R+"*([>+~]|"+R+")"+R+"*"),X=RegExp(R+"*[+~]"),U=RegExp("="+R+"*([^\\]'\"]*)"+R+"*\\]","g"),Y=RegExp(B),V=RegExp("^"+W+"$"),G={ID:RegExp("^#("+M+")"),CLASS:RegExp("^\\.("+M+")"),TAG:RegExp("^("+M.replace("w","w*")+")"),ATTR:RegExp("^"+$),PSEUDO:RegExp("^"+B),CHILD:RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+R+"*(even|odd|(([+-]|)(\\d*)n|)"+R+"*(?:([+-]|)"+R+"*(\\d+)|))"+R+"*\\)|)","i"),"boolean":RegExp("^(?:"+P+")$","i"),needsContext:RegExp("^"+R+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+R+"*((?:-\\d)?\\d*)"+R+"*\\)|)(?=[^-]|$)","i")},J=/^[^{]+\{\s*\[native \w/,Q=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,K=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,et=/'|\\/g,tt=/\\([\da-fA-F]{1,6}[\x20\t\r\n\f]?|.)/g,nt=function(e,t){var n="0x"+t-65536;return n!==n?t:0>n?String.fromCharCode(n+65536):String.fromCharCode(55296|n>>10,56320|1023&n)};try{H.apply(A=O.call(v.childNodes),v.childNodes),A[v.childNodes.length].nodeType}catch(rt){H={apply:A.length?function(e,t){q.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function it(e){return J.test(e+"")}function ot(){var e,t=[];return e=function(n,i){return t.push(n+=" ")>r.cacheLength&&delete e[t.shift()],e[n]=i}}function st(e){return e[y]=!0,e}function at(e){var t=c.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function ut(e,t,n,r){var i,o,s,a,u,f,d,g,x,w;if((t?t.ownerDocument||t:v)!==c&&l(t),t=t||c,n=n||[],!e||"string"!=typeof e)return n;if(1!==(a=t.nodeType)&&9!==a)return[];if(p&&!r){if(i=Q.exec(e))if(s=i[1]){if(9===a){if(o=t.getElementById(s),!o||!o.parentNode)return n;if(o.id===s)return n.push(o),n}else if(t.ownerDocument&&(o=t.ownerDocument.getElementById(s))&&m(t,o)&&o.id===s)return n.push(o),n}else{if(i[2])return H.apply(n,t.getElementsByTagName(e)),n;if((s=i[3])&&b.getElementsByClassName&&t.getElementsByClassName)return H.apply(n,t.getElementsByClassName(s)),n}if(b.qsa&&(!h||!h.test(e))){if(g=d=y,x=t,w=9===a&&e,1===a&&"object"!==t.nodeName.toLowerCase()){f=gt(e),(d=t.getAttribute("id"))?g=d.replace(et,"\\$&"):t.setAttribute("id",g),g="[id='"+g+"'] ",u=f.length;while(u--)f[u]=g+mt(f[u]);x=X.test(e)&&t.parentNode||t,w=f.join(",")}if(w)try{return H.apply(n,x.querySelectorAll(w)),n}catch(T){}finally{d||t.removeAttribute("id")}}}return kt(e.replace(I,"$1"),t,n,r)}o=ut.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},l=ut.setDocument=function(e){var t=e?e.ownerDocument||e:v;return t!==c&&9===t.nodeType&&t.documentElement?(c=t,f=t.documentElement,p=!o(t),b.getElementsByTagName=at(function(e){return e.appendChild(t.createComment("")),!e.getElementsByTagName("*").length}),b.attributes=at(function(e){return e.className="i",!e.getAttribute("className")}),b.getElementsByClassName=at(function(e){return e.innerHTML="",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),b.sortDetached=at(function(e){return 1&e.compareDocumentPosition(c.createElement("div"))}),b.getById=at(function(e){return f.appendChild(e).id=y,!t.getElementsByName||!t.getElementsByName(y).length}),b.getById?(r.find.ID=function(e,t){if(typeof t.getElementById!==j&&p){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},r.filter.ID=function(e){var t=e.replace(tt,nt);return function(e){return e.getAttribute("id")===t}}):(r.find.ID=function(e,t){if(typeof t.getElementById!==j&&p){var n=t.getElementById(e);return n?n.id===e||typeof n.getAttributeNode!==j&&n.getAttributeNode("id").value===e?[n]:undefined:[]}},r.filter.ID=function(e){var t=e.replace(tt,nt);return function(e){var n=typeof e.getAttributeNode!==j&&e.getAttributeNode("id");return n&&n.value===t}}),r.find.TAG=b.getElementsByTagName?function(e,t){return typeof t.getElementsByTagName!==j?t.getElementsByTagName(e):undefined}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},r.find.CLASS=b.getElementsByClassName&&function(e,t){return typeof t.getElementsByClassName!==j&&p?t.getElementsByClassName(e):undefined},d=[],h=[],(b.qsa=it(t.querySelectorAll))&&(at(function(e){e.innerHTML="",e.querySelectorAll("[selected]").length||h.push("\\["+R+"*(?:value|"+P+")"),e.querySelectorAll(":checked").length||h.push(":checked")}),at(function(e){var t=c.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("t",""),e.querySelectorAll("[t^='']").length&&h.push("[*^$]="+R+"*(?:''|\"\")"),e.querySelectorAll(":enabled").length||h.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),h.push(",.*:")})),(b.matchesSelector=it(g=f.webkitMatchesSelector||f.mozMatchesSelector||f.oMatchesSelector||f.msMatchesSelector))&&at(function(e){b.disconnectedMatch=g.call(e,"div"),g.call(e,"[s!='']:x"),d.push("!=",B)}),h=h.length&&RegExp(h.join("|")),d=d.length&&RegExp(d.join("|")),m=it(f.contains)||f.compareDocumentPosition?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},S=f.compareDocumentPosition?function(e,n){if(e===n)return E=!0,0;var r=n.compareDocumentPosition&&e.compareDocumentPosition&&e.compareDocumentPosition(n);return r?1&r||!b.sortDetached&&n.compareDocumentPosition(e)===r?e===t||m(v,e)?-1:n===t||m(v,n)?1:u?F.call(u,e)-F.call(u,n):0:4&r?-1:1:e.compareDocumentPosition?-1:1}:function(e,n){var r,i=0,o=e.parentNode,s=n.parentNode,a=[e],l=[n];if(e===n)return E=!0,0;if(!o||!s)return e===t?-1:n===t?1:o?-1:s?1:u?F.call(u,e)-F.call(u,n):0;if(o===s)return lt(e,n);r=e;while(r=r.parentNode)a.unshift(r);r=n;while(r=r.parentNode)l.unshift(r);while(a[i]===l[i])i++;return i?lt(a[i],l[i]):a[i]===v?-1:l[i]===v?1:0},c):c},ut.matches=function(e,t){return ut(e,null,null,t)},ut.matchesSelector=function(e,t){if((e.ownerDocument||e)!==c&&l(e),t=t.replace(U,"='$1']"),!(!b.matchesSelector||!p||d&&d.test(t)||h&&h.test(t)))try{var n=g.call(e,t);if(n||b.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(r){}return ut(t,c,null,[e]).length>0},ut.contains=function(e,t){return(e.ownerDocument||e)!==c&&l(e),m(e,t)},ut.attr=function(e,t){(e.ownerDocument||e)!==c&&l(e);var n=r.attrHandle[t.toLowerCase()],i=n&&n(e,t,!p);return i===undefined?b.attributes||!p?e.getAttribute(t):(i=e.getAttributeNode(t))&&i.specified?i.value:null:i},ut.error=function(e){throw Error("Syntax error, unrecognized expression: "+e)},ut.uniqueSort=function(e){var t,n=[],r=0,i=0;if(E=!b.detectDuplicates,u=!b.sortStable&&e.slice(0),e.sort(S),E){while(t=e[i++])t===e[i]&&(r=n.push(i));while(r--)e.splice(n[r],1)}return e};function lt(e,t){var n=t&&e,r=n&&(~t.sourceIndex||D)-(~e.sourceIndex||D);if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function ct(e,t,n){var r;return n?undefined:(r=e.getAttributeNode(t))&&r.specified?r.value:e[t]===!0?t.toLowerCase():null}function ft(e,t,n){var r;return n?undefined:r=e.getAttribute(t,"type"===t.toLowerCase()?1:2)}function pt(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function ht(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function dt(e){return st(function(t){return t=+t,st(function(n,r){var i,o=e([],n.length,t),s=o.length;while(s--)n[i=o[s]]&&(n[i]=!(r[i]=n[i]))})})}i=ut.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=i(e)}else if(3===o||4===o)return e.nodeValue}else for(;t=e[r];r++)n+=i(t);return n},r=ut.selectors={cacheLength:50,createPseudo:st,match:G,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(tt,nt),e[3]=(e[4]||e[5]||"").replace(tt,nt),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||ut.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&ut.error(e[0]),e},PSEUDO:function(e){var t,n=!e[5]&&e[2];return G.CHILD.test(e[0])?null:(e[4]?e[2]=e[4]:n&&Y.test(n)&&(t=gt(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(tt,nt).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=C[e+" "];return t||(t=RegExp("(^|"+R+")"+e+"("+R+"|$)"))&&C(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==j&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=ut.attr(r,e);return null==i?"!="===t:t?(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i+" ").indexOf(n)>-1:"|="===t?i===n||i.slice(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),s="last"!==e.slice(-4),a="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,p,h,d,g=o!==s?"nextSibling":"previousSibling",m=t.parentNode,v=a&&t.nodeName.toLowerCase(),x=!u&&!a;if(m){if(o){while(g){f=t;while(f=f[g])if(a?f.nodeName.toLowerCase()===v:1===f.nodeType)return!1;d=g="only"===e&&!d&&"nextSibling"}return!0}if(d=[s?m.firstChild:m.lastChild],s&&x){c=m[y]||(m[y]={}),l=c[e]||[],h=l[0]===w&&l[1],p=l[0]===w&&l[2],f=h&&m.childNodes[h];while(f=++h&&f&&f[g]||(p=h=0)||d.pop())if(1===f.nodeType&&++p&&f===t){c[e]=[w,h,p];break}}else if(x&&(l=(t[y]||(t[y]={}))[e])&&l[0]===w)p=l[1];else while(f=++h&&f&&f[g]||(p=h=0)||d.pop())if((a?f.nodeName.toLowerCase()===v:1===f.nodeType)&&++p&&(x&&((f[y]||(f[y]={}))[e]=[w,p]),f===t))break;return p-=i,p===r||0===p%r&&p/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||ut.error("unsupported pseudo: "+e);return i[y]?i(t):i.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?st(function(e,n){var r,o=i(e,t),s=o.length;while(s--)r=F.call(e,o[s]),e[r]=!(n[r]=o[s])}):function(e){return i(e,0,n)}):i}},pseudos:{not:st(function(e){var t=[],n=[],r=s(e.replace(I,"$1"));return r[y]?st(function(e,t,n,i){var o,s=r(e,null,i,[]),a=e.length;while(a--)(o=s[a])&&(e[a]=!(t[a]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),!n.pop()}}),has:st(function(e){return function(t){return ut(e,t).length>0}}),contains:st(function(e){return function(t){return(t.textContent||t.innerText||i(t)).indexOf(e)>-1}}),lang:st(function(e){return V.test(e||"")||ut.error("unsupported lang: "+e),e=e.replace(tt,nt).toLowerCase(),function(t){var n;do if(n=p?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===f},focus:function(e){return e===c.activeElement&&(!c.hasFocus||c.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeName>"@"||3===e.nodeType||4===e.nodeType)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return Z.test(e.nodeName)},input:function(e){return K.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||t.toLowerCase()===e.type)},first:dt(function(){return[0]}),last:dt(function(e,t){return[t-1]}),eq:dt(function(e,t,n){return[0>n?n+t:n]}),even:dt(function(e,t){var n=0;for(;t>n;n+=2)e.push(n);return e}),odd:dt(function(e,t){var n=1;for(;t>n;n+=2)e.push(n);return e}),lt:dt(function(e,t,n){var r=0>n?n+t:n;for(;--r>=0;)e.push(r);return e}),gt:dt(function(e,t,n){var r=0>n?n+t:n;for(;t>++r;)e.push(r);return e})}};for(t in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})r.pseudos[t]=pt(t);for(t in{submit:!0,reset:!0})r.pseudos[t]=ht(t);function gt(e,t){var n,i,o,s,a,u,l,c=k[e+" "];if(c)return t?0:c.slice(0);a=e,u=[],l=r.preFilter;while(a){(!n||(i=z.exec(a)))&&(i&&(a=a.slice(i[0].length)||a),u.push(o=[])),n=!1,(i=_.exec(a))&&(n=i.shift(),o.push({value:n,type:i[0].replace(I," ")}),a=a.slice(n.length));for(s in r.filter)!(i=G[s].exec(a))||l[s]&&!(i=l[s](i))||(n=i.shift(),o.push({value:n,type:s,matches:i}),a=a.slice(n.length));if(!n)break}return t?a.length:a?ut.error(e):k(e,u).slice(0)}function mt(e){var t=0,n=e.length,r="";for(;n>t;t++)r+=e[t].value;return r}function yt(e,t,r){var i=t.dir,o=r&&"parentNode"===i,s=T++;return t.first?function(t,n,r){while(t=t[i])if(1===t.nodeType||o)return e(t,n,r)}:function(t,r,a){var u,l,c,f=w+" "+s;if(a){while(t=t[i])if((1===t.nodeType||o)&&e(t,r,a))return!0}else while(t=t[i])if(1===t.nodeType||o)if(c=t[y]||(t[y]={}),(l=c[i])&&l[0]===f){if((u=l[1])===!0||u===n)return u===!0}else if(l=c[i]=[f],l[1]=e(t,r,a)||n,l[1]===!0)return!0}}function vt(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function xt(e,t,n,r,i){var o,s=[],a=0,u=e.length,l=null!=t;for(;u>a;a++)(o=e[a])&&(!n||n(o,r,i))&&(s.push(o),l&&t.push(a));return s}function bt(e,t,n,r,i,o){return r&&!r[y]&&(r=bt(r)),i&&!i[y]&&(i=bt(i,o)),st(function(o,s,a,u){var l,c,f,p=[],h=[],d=s.length,g=o||Ct(t||"*",a.nodeType?[a]:a,[]),m=!e||!o&&t?g:xt(g,p,e,a,u),y=n?i||(o?e:d||r)?[]:s:m;if(n&&n(m,y,a,u),r){l=xt(y,h),r(l,[],a,u),c=l.length;while(c--)(f=l[c])&&(y[h[c]]=!(m[h[c]]=f))}if(o){if(i||e){if(i){l=[],c=y.length;while(c--)(f=y[c])&&l.push(m[c]=f);i(null,y=[],l,u)}c=y.length;while(c--)(f=y[c])&&(l=i?F.call(o,f):p[c])>-1&&(o[l]=!(s[l]=f))}}else y=xt(y===s?y.splice(d,y.length):y),i?i(null,s,y,u):H.apply(s,y)})}function wt(e){var t,n,i,o=e.length,s=r.relative[e[0].type],u=s||r.relative[" "],l=s?1:0,c=yt(function(e){return e===t},u,!0),f=yt(function(e){return F.call(t,e)>-1},u,!0),p=[function(e,n,r){return!s&&(r||n!==a)||((t=n).nodeType?c(e,n,r):f(e,n,r))}];for(;o>l;l++)if(n=r.relative[e[l].type])p=[yt(vt(p),n)];else{if(n=r.filter[e[l].type].apply(null,e[l].matches),n[y]){for(i=++l;o>i;i++)if(r.relative[e[i].type])break;return bt(l>1&&vt(p),l>1&&mt(e.slice(0,l-1)).replace(I,"$1"),n,i>l&&wt(e.slice(l,i)),o>i&&wt(e=e.slice(i)),o>i&&mt(e))}p.push(n)}return vt(p)}function Tt(e,t){var i=0,o=t.length>0,s=e.length>0,u=function(u,l,f,p,h){var d,g,m,y=[],v=0,x="0",b=u&&[],T=null!=h,C=a,k=u||s&&r.find.TAG("*",h&&l.parentNode||l),N=w+=null==C?1:Math.random()||.1;for(T&&(a=l!==c&&l,n=i);null!=(d=k[x]);x++){if(s&&d){g=0;while(m=e[g++])if(m(d,l,f)){p.push(d);break}T&&(w=N,n=++i)}o&&((d=!m&&d)&&v--,u&&b.push(d))}if(v+=x,o&&x!==v){g=0;while(m=t[g++])m(b,y,l,f);if(u){if(v>0)while(x--)b[x]||y[x]||(y[x]=L.call(p));y=xt(y)}H.apply(p,y),T&&!u&&y.length>0&&v+t.length>1&&ut.uniqueSort(p)}return T&&(w=N,a=C),b};return o?st(u):u}s=ut.compile=function(e,t){var n,r=[],i=[],o=N[e+" "];if(!o){t||(t=gt(e)),n=t.length;while(n--)o=wt(t[n]),o[y]?r.push(o):i.push(o);o=N(e,Tt(i,r))}return o};function Ct(e,t,n){var r=0,i=t.length;for(;i>r;r++)ut(e,t[r],n);return n}function kt(e,t,n,i){var o,a,u,l,c,f=gt(e);if(!i&&1===f.length){if(a=f[0]=f[0].slice(0),a.length>2&&"ID"===(u=a[0]).type&&9===t.nodeType&&p&&r.relative[a[1].type]){if(t=(r.find.ID(u.matches[0].replace(tt,nt),t)||[])[0],!t)return n;e=e.slice(a.shift().value.length)}o=G.needsContext.test(e)?0:a.length;while(o--){if(u=a[o],r.relative[l=u.type])break;if((c=r.find[l])&&(i=c(u.matches[0].replace(tt,nt),X.test(a[0].type)&&t.parentNode||t))){if(a.splice(o,1),e=i.length&&mt(a),!e)return H.apply(n,i),n;break}}}return s(e,f)(i,t,!p,n,X.test(e)),n}r.pseudos.nth=r.pseudos.eq;function Nt(){}Nt.prototype=r.filters=r.pseudos,r.setFilters=new Nt,b.sortStable=y.split("").sort(S).join("")===y,l(),[0,0].sort(S),b.detectDuplicates=E,at(function(e){if(e.innerHTML="","#"!==e.firstChild.getAttribute("href")){var t="type|href|height|width".split("|"),n=t.length;while(n--)r.attrHandle[t[n]]=ft}}),at(function(e){if(null!=e.getAttribute("disabled")){var t=P.split("|"),n=t.length;while(n--)r.attrHandle[t[n]]=ct}}),x.find=ut,x.expr=ut.selectors,x.expr[":"]=x.expr.pseudos,x.unique=ut.uniqueSort,x.text=ut.getText,x.isXMLDoc=ut.isXML,x.contains=ut.contains}(e);var D={};function A(e){var t=D[e]={};return x.each(e.match(w)||[],function(e,n){t[n]=!0}),t}x.Callbacks=function(e){e="string"==typeof e?D[e]||A(e):x.extend({},e);var t,n,r,i,o,s,a=[],u=!e.once&&[],l=function(f){for(t=e.memory&&f,n=!0,s=i||0,i=0,o=a.length,r=!0;a&&o>s;s++)if(a[s].apply(f[0],f[1])===!1&&e.stopOnFalse){t=!1;break}r=!1,a&&(u?u.length&&l(u.shift()):t?a=[]:c.disable())},c={add:function(){if(a){var n=a.length;(function s(t){x.each(t,function(t,n){var r=x.type(n);"function"===r?e.unique&&c.has(n)||a.push(n):n&&n.length&&"string"!==r&&s(n)})})(arguments),r?o=a.length:t&&(i=n,l(t))}return this},remove:function(){return a&&x.each(arguments,function(e,t){var n;while((n=x.inArray(t,a,n))>-1)a.splice(n,1),r&&(o>=n&&o--,s>=n&&s--)}),this},has:function(e){return e?x.inArray(e,a)>-1:!(!a||!a.length)},empty:function(){return a=[],o=0,this},disable:function(){return a=u=t=undefined,this},disabled:function(){return!a},lock:function(){return u=undefined,t||c.disable(),this},locked:function(){return!u},fireWith:function(e,t){return t=t||[],t=[e,t.slice?t.slice():t],!a||n&&!u||(r?u.push(t):l(t)),this},fire:function(){return c.fireWith(this,arguments),this},fired:function(){return!!n}};return c},x.extend({Deferred:function(e){var t=[["resolve","done",x.Callbacks("once memory"),"resolved"],["reject","fail",x.Callbacks("once memory"),"rejected"],["notify","progress",x.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return x.Deferred(function(n){x.each(t,function(t,o){var s=o[0],a=x.isFunction(e[t])&&e[t];i[o[1]](function(){var e=a&&a.apply(this,arguments);e&&x.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[s+"With"](this===r?n.promise():this,a?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?x.extend(e,r):r}},i={};return r.pipe=r.then,x.each(t,function(e,o){var s=o[2],a=o[3];r[o[1]]=s.add,a&&s.add(function(){n=a},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=s.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=d.call(arguments),r=n.length,i=1!==r||e&&x.isFunction(e.promise)?r:0,o=1===i?e:x.Deferred(),s=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?d.call(arguments):r,n===a?o.notifyWith(t,n):--i||o.resolveWith(t,n)}},a,u,l;if(r>1)for(a=Array(r),u=Array(r),l=Array(r);r>t;t++)n[t]&&x.isFunction(n[t].promise)?n[t].promise().done(s(t,l,n)).fail(o.reject).progress(s(t,u,a)):--i;return i||o.resolveWith(l,n),o.promise()}}),x.support=function(t){var n=o.createElement("input"),r=o.createDocumentFragment(),i=o.createElement("div"),s=o.createElement("select"),a=s.appendChild(o.createElement("option"));return n.type?(n.type="checkbox",t.checkOn=""!==n.value,t.optSelected=a.selected,t.reliableMarginRight=!0,t.boxSizingReliable=!0,t.pixelPosition=!1,n.checked=!0,t.noCloneChecked=n.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!a.disabled,n=o.createElement("input"),n.value="t",n.type="radio",t.radioValue="t"===n.value,n.setAttribute("checked","t"),n.setAttribute("name","t"),r.appendChild(n),t.checkClone=r.cloneNode(!0).cloneNode(!0).lastChild.checked,t.focusinBubbles="onfocusin"in e,i.style.backgroundClip="content-box",i.cloneNode(!0).style.backgroundClip="",t.clearCloneStyle="content-box"===i.style.backgroundClip,x(function(){var n,r,s="padding:0;margin:0;border:0;display:block;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box",a=o.getElementsByTagName("body")[0];a&&(n=o.createElement("div"),n.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",a.appendChild(n).appendChild(i),i.innerHTML="",i.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%",x.swap(a,null!=a.style.zoom?{zoom:1}:{},function(){t.boxSizing=4===i.offsetWidth}),e.getComputedStyle&&(t.pixelPosition="1%"!==(e.getComputedStyle(i,null)||{}).top,t.boxSizingReliable="4px"===(e.getComputedStyle(i,null)||{width:"4px"}).width,r=i.appendChild(o.createElement("div")),r.style.cssText=i.style.cssText=s,r.style.marginRight=r.style.width="0",i.style.width="1px",t.reliableMarginRight=!parseFloat((e.getComputedStyle(r,null)||{}).marginRight)),a.removeChild(n))}),t):t}({});var L,q,H=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,O=/([A-Z])/g;function F(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=x.expando+Math.random()}F.uid=1,F.accepts=function(e){return e.nodeType?1===e.nodeType||9===e.nodeType:!0},F.prototype={key:function(e){if(!F.accepts(e))return 0;var t={},n=e[this.expando];if(!n){n=F.uid++;try{t[this.expando]={value:n},Object.defineProperties(e,t)}catch(r){t[this.expando]=n,x.extend(e,t)}}return this.cache[n]||(this.cache[n]={}),n},set:function(e,t,n){var r,i=this.key(e),o=this.cache[i];if("string"==typeof t)o[t]=n;else if(x.isEmptyObject(o))this.cache[i]=t;else for(r in t)o[r]=t[r]},get:function(e,t){var n=this.cache[this.key(e)];return t===undefined?n:n[t]},access:function(e,t,n){return t===undefined||t&&"string"==typeof t&&n===undefined?this.get(e,t):(this.set(e,t,n),n!==undefined?n:t)},remove:function(e,t){var n,r,i=this.key(e),o=this.cache[i];if(t===undefined)this.cache[i]={};else{x.isArray(t)?r=t.concat(t.map(x.camelCase)):t in o?r=[t]:(r=x.camelCase(t),r=r in o?[r]:r.match(w)||[]),n=r.length;while(n--)delete o[r[n]]}},hasData:function(e){return!x.isEmptyObject(this.cache[e[this.expando]]||{})},discard:function(e){delete this.cache[this.key(e)]}},L=new F,q=new F,x.extend({acceptData:F.accepts,hasData:function(e){return L.hasData(e)||q.hasData(e)},data:function(e,t,n){return L.access(e,t,n)},removeData:function(e,t){L.remove(e,t)},_data:function(e,t,n){return q.access(e,t,n)},_removeData:function(e,t){q.remove(e,t)}}),x.fn.extend({data:function(e,t){var n,r,i=this[0],o=0,s=null;if(e===undefined){if(this.length&&(s=L.get(i),1===i.nodeType&&!q.get(i,"hasDataAttrs"))){for(n=i.attributes;n.length>o;o++)r=n[o].name,0===r.indexOf("data-")&&(r=x.camelCase(r.substring(5)),P(i,r,s[r]));q.set(i,"hasDataAttrs",!0)}return s}return"object"==typeof e?this.each(function(){L.set(this,e)}):x.access(this,function(t){var n,r=x.camelCase(e);if(i&&t===undefined){if(n=L.get(i,e),n!==undefined)return n;if(n=L.get(i,r),n!==undefined)return n;if(n=P(i,r,undefined),n!==undefined)return n}else this.each(function(){var n=L.get(this,r);L.set(this,r,t),-1!==e.indexOf("-")&&n!==undefined&&L.set(this,e,t)})},null,t,arguments.length>1,null,!0)},removeData:function(e){return this.each(function(){L.remove(this,e)})}});function P(e,t,n){var r;if(n===undefined&&1===e.nodeType)if(r="data-"+t.replace(O,"-$1").toLowerCase(),n=e.getAttribute(r),"string"==typeof n){try{n="true"===n?!0:"false"===n?!1:"null"===n?null:+n+""===n?+n:H.test(n)?JSON.parse(n):n}catch(i){}L.set(e,t,n)}else n=undefined;return n}x.extend({queue:function(e,t,n){var r;return e?(t=(t||"fx")+"queue",r=q.get(e,t),n&&(!r||x.isArray(n)?r=q.access(e,t,x.makeArray(n)):r.push(n)),r||[]):undefined},dequeue:function(e,t){t=t||"fx";var n=x.queue(e,t),r=n.length,i=n.shift(),o=x._queueHooks(e,t),s=function(){x.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),o.cur=i,i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,s,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return q.get(e,n)||q.access(e,n,{empty:x.Callbacks("once memory").add(function(){q.remove(e,[t+"queue",n])})})}}),x.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),n>arguments.length?x.queue(this[0],e):t===undefined?this:this.each(function(){var n=x.queue(this,e,t);
+x._queueHooks(this,e),"fx"===e&&"inprogress"!==n[0]&&x.dequeue(this,e)})},dequeue:function(e){return this.each(function(){x.dequeue(this,e)})},delay:function(e,t){return e=x.fx?x.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,t){var n,r=1,i=x.Deferred(),o=this,s=this.length,a=function(){--r||i.resolveWith(o,[o])};"string"!=typeof e&&(t=e,e=undefined),e=e||"fx";while(s--)n=q.get(o[s],e+"queueHooks"),n&&n.empty&&(r++,n.empty.add(a));return a(),i.promise(t)}});var R,M,W=/[\t\r\n]/g,$=/\r/g,B=/^(?:input|select|textarea|button)$/i;x.fn.extend({attr:function(e,t){return x.access(this,x.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){x.removeAttr(this,e)})},prop:function(e,t){return x.access(this,x.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[x.propFix[e]||e]})},addClass:function(e){var t,n,r,i,o,s=0,a=this.length,u="string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).addClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(w)||[];a>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(W," "):" ")){o=0;while(i=t[o++])0>r.indexOf(" "+i+" ")&&(r+=i+" ");n.className=x.trim(r)}return this},removeClass:function(e){var t,n,r,i,o,s=0,a=this.length,u=0===arguments.length||"string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).removeClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(w)||[];a>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(W," "):"")){o=0;while(i=t[o++])while(r.indexOf(" "+i+" ")>=0)r=r.replace(" "+i+" "," ");n.className=e?x.trim(r):""}return this},toggleClass:function(e,t){var n=typeof e,i="boolean"==typeof t;return x.isFunction(e)?this.each(function(n){x(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if("string"===n){var o,s=0,a=x(this),u=t,l=e.match(w)||[];while(o=l[s++])u=i?u:!a.hasClass(o),a[u?"addClass":"removeClass"](o)}else(n===r||"boolean"===n)&&(this.className&&q.set(this,"__className__",this.className),this.className=this.className||e===!1?"":q.get(this,"__className__")||"")})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(W," ").indexOf(t)>=0)return!0;return!1},val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=x.isFunction(e),this.each(function(n){var i,o=x(this);1===this.nodeType&&(i=r?e.call(this,n,o.val()):e,null==i?i="":"number"==typeof i?i+="":x.isArray(i)&&(i=x.map(i,function(e){return null==e?"":e+""})),t=x.valHooks[this.type]||x.valHooks[this.nodeName.toLowerCase()],t&&"set"in t&&t.set(this,i,"value")!==undefined||(this.value=i))});if(i)return t=x.valHooks[i.type]||x.valHooks[i.nodeName.toLowerCase()],t&&"get"in t&&(n=t.get(i,"value"))!==undefined?n:(n=i.value,"string"==typeof n?n.replace($,""):null==n?"":n)}}}),x.extend({valHooks:{option:{get:function(e){var t=e.attributes.value;return!t||t.specified?e.value:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,s=o?null:[],a=o?i+1:r.length,u=0>i?a:o?i:0;for(;a>u;u++)if(n=r[u],!(!n.selected&&u!==i||(x.support.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&x.nodeName(n.parentNode,"optgroup"))){if(t=x(n).val(),o)return t;s.push(t)}return s},set:function(e,t){var n,r,i=e.options,o=x.makeArray(t),s=i.length;while(s--)r=i[s],(r.selected=x.inArray(x(r).val(),o)>=0)&&(n=!0);return n||(e.selectedIndex=-1),o}}},attr:function(e,t,n){var i,o,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return typeof e.getAttribute===r?x.prop(e,t,n):(1===s&&x.isXMLDoc(e)||(t=t.toLowerCase(),i=x.attrHooks[t]||(x.expr.match.boolean.test(t)?M:R)),n===undefined?i&&"get"in i&&null!==(o=i.get(e,t))?o:(o=x.find.attr(e,t),null==o?undefined:o):null!==n?i&&"set"in i&&(o=i.set(e,n,t))!==undefined?o:(e.setAttribute(t,n+""),n):(x.removeAttr(e,t),undefined))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(w);if(o&&1===e.nodeType)while(n=o[i++])r=x.propFix[n]||n,x.expr.match.boolean.test(n)&&(e[r]=!1),e.removeAttribute(n)},attrHooks:{type:{set:function(e,t){if(!x.support.radioValue&&"radio"===t&&x.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},propFix:{"for":"htmlFor","class":"className"},prop:function(e,t,n){var r,i,o,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return o=1!==s||!x.isXMLDoc(e),o&&(t=x.propFix[t]||t,i=x.propHooks[t]),n!==undefined?i&&"set"in i&&(r=i.set(e,n,t))!==undefined?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){return e.hasAttribute("tabindex")||B.test(e.nodeName)||e.href?e.tabIndex:-1}}}}),M={set:function(e,t,n){return t===!1?x.removeAttr(e,n):e.setAttribute(n,n),n}},x.each(x.expr.match.boolean.source.match(/\w+/g),function(e,t){var n=x.expr.attrHandle[t]||x.find.attr;x.expr.attrHandle[t]=function(e,t,r){var i=x.expr.attrHandle[t],o=r?undefined:(x.expr.attrHandle[t]=undefined)!=n(e,t,r)?t.toLowerCase():null;return x.expr.attrHandle[t]=i,o}}),x.support.optSelected||(x.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null}}),x.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){x.propFix[this.toLowerCase()]=this}),x.each(["radio","checkbox"],function(){x.valHooks[this]={set:function(e,t){return x.isArray(t)?e.checked=x.inArray(x(e).val(),t)>=0:undefined}},x.support.checkOn||(x.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var I=/^key/,z=/^(?:mouse|contextmenu)|click/,_=/^(?:focusinfocus|focusoutblur)$/,X=/^([^.]*)(?:\.(.+)|)$/;function U(){return!0}function Y(){return!1}function V(){try{return o.activeElement}catch(e){}}x.event={global:{},add:function(e,t,n,i,o){var s,a,u,l,c,f,p,h,d,g,m,y=q.get(e);if(y){n.handler&&(s=n,n=s.handler,o=s.selector),n.guid||(n.guid=x.guid++),(l=y.events)||(l=y.events={}),(a=y.handle)||(a=y.handle=function(e){return typeof x===r||e&&x.event.triggered===e.type?undefined:x.event.dispatch.apply(a.elem,arguments)},a.elem=e),t=(t||"").match(w)||[""],c=t.length;while(c--)u=X.exec(t[c])||[],d=m=u[1],g=(u[2]||"").split(".").sort(),d&&(p=x.event.special[d]||{},d=(o?p.delegateType:p.bindType)||d,p=x.event.special[d]||{},f=x.extend({type:d,origType:m,data:i,handler:n,guid:n.guid,selector:o,needsContext:o&&x.expr.match.needsContext.test(o),namespace:g.join(".")},s),(h=l[d])||(h=l[d]=[],h.delegateCount=0,p.setup&&p.setup.call(e,i,g,a)!==!1||e.addEventListener&&e.addEventListener(d,a,!1)),p.add&&(p.add.call(e,f),f.handler.guid||(f.handler.guid=n.guid)),o?h.splice(h.delegateCount++,0,f):h.push(f),x.event.global[d]=!0);e=null}},remove:function(e,t,n,r,i){var o,s,a,u,l,c,f,p,h,d,g,m=q.hasData(e)&&q.get(e);if(m&&(u=m.events)){t=(t||"").match(w)||[""],l=t.length;while(l--)if(a=X.exec(t[l])||[],h=g=a[1],d=(a[2]||"").split(".").sort(),h){f=x.event.special[h]||{},h=(r?f.delegateType:f.bindType)||h,p=u[h]||[],a=a[2]&&RegExp("(^|\\.)"+d.join("\\.(?:.*\\.|)")+"(\\.|$)"),s=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||a&&!a.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));s&&!p.length&&(f.teardown&&f.teardown.call(e,d,m.handle)!==!1||x.removeEvent(e,h,m.handle),delete u[h])}else for(h in u)x.event.remove(e,h+t[l],n,r,!0);x.isEmptyObject(u)&&(delete m.handle,q.remove(e,"events"))}},trigger:function(t,n,r,i){var s,a,u,l,c,f,p,h=[r||o],d=y.call(t,"type")?t.type:t,g=y.call(t,"namespace")?t.namespace.split("."):[];if(a=u=r=r||o,3!==r.nodeType&&8!==r.nodeType&&!_.test(d+x.event.triggered)&&(d.indexOf(".")>=0&&(g=d.split("."),d=g.shift(),g.sort()),c=0>d.indexOf(":")&&"on"+d,t=t[x.expando]?t:new x.Event(d,"object"==typeof t&&t),t.isTrigger=i?2:3,t.namespace=g.join("."),t.namespace_re=t.namespace?RegExp("(^|\\.)"+g.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=undefined,t.target||(t.target=r),n=null==n?[t]:x.makeArray(n,[t]),p=x.event.special[d]||{},i||!p.trigger||p.trigger.apply(r,n)!==!1)){if(!i&&!p.noBubble&&!x.isWindow(r)){for(l=p.delegateType||d,_.test(l+d)||(a=a.parentNode);a;a=a.parentNode)h.push(a),u=a;u===(r.ownerDocument||o)&&h.push(u.defaultView||u.parentWindow||e)}s=0;while((a=h[s++])&&!t.isPropagationStopped())t.type=s>1?l:p.bindType||d,f=(q.get(a,"events")||{})[t.type]&&q.get(a,"handle"),f&&f.apply(a,n),f=c&&a[c],f&&x.acceptData(a)&&f.apply&&f.apply(a,n)===!1&&t.preventDefault();return t.type=d,i||t.isDefaultPrevented()||p._default&&p._default.apply(h.pop(),n)!==!1||!x.acceptData(r)||c&&x.isFunction(r[d])&&!x.isWindow(r)&&(u=r[c],u&&(r[c]=null),x.event.triggered=d,r[d](),x.event.triggered=undefined,u&&(r[c]=u)),t.result}},dispatch:function(e){e=x.event.fix(e);var t,n,r,i,o,s=[],a=d.call(arguments),u=(q.get(this,"events")||{})[e.type]||[],l=x.event.special[e.type]||{};if(a[0]=e,e.delegateTarget=this,!l.preDispatch||l.preDispatch.call(this,e)!==!1){s=x.event.handlers.call(this,e,u),t=0;while((i=s[t++])&&!e.isPropagationStopped()){e.currentTarget=i.elem,n=0;while((o=i.handlers[n++])&&!e.isImmediatePropagationStopped())(!e.namespace_re||e.namespace_re.test(o.namespace))&&(e.handleObj=o,e.data=o.data,r=((x.event.special[o.origType]||{}).handle||o.handler).apply(i.elem,a),r!==undefined&&(e.result=r)===!1&&(e.preventDefault(),e.stopPropagation()))}return l.postDispatch&&l.postDispatch.call(this,e),e.result}},handlers:function(e,t){var n,r,i,o,s=[],a=t.delegateCount,u=e.target;if(a&&u.nodeType&&(!e.button||"click"!==e.type))for(;u!==this;u=u.parentNode||this)if(u.disabled!==!0||"click"!==e.type){for(r=[],n=0;a>n;n++)o=t[n],i=o.selector+" ",r[i]===undefined&&(r[i]=o.needsContext?x(i,this).index(u)>=0:x.find(i,this,null,[u]).length),r[i]&&r.push(o);r.length&&s.push({elem:u,handlers:r})}return t.length>a&&s.push({elem:this,handlers:t.slice(a)}),s},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,t){var n,r,i,s=t.button;return null==e.pageX&&null!=t.clientX&&(n=e.target.ownerDocument||o,r=n.documentElement,i=n.body,e.pageX=t.clientX+(r&&r.scrollLeft||i&&i.scrollLeft||0)-(r&&r.clientLeft||i&&i.clientLeft||0),e.pageY=t.clientY+(r&&r.scrollTop||i&&i.scrollTop||0)-(r&&r.clientTop||i&&i.clientTop||0)),e.which||s===undefined||(e.which=1&s?1:2&s?3:4&s?2:0),e}},fix:function(e){if(e[x.expando])return e;var t,n,r,i=e.type,o=e,s=this.fixHooks[i];s||(this.fixHooks[i]=s=z.test(i)?this.mouseHooks:I.test(i)?this.keyHooks:{}),r=s.props?this.props.concat(s.props):this.props,e=new x.Event(o),t=r.length;while(t--)n=r[t],e[n]=o[n];return 3===e.target.nodeType&&(e.target=e.target.parentNode),s.filter?s.filter(e,o):e},special:{load:{noBubble:!0},focus:{trigger:function(){return this!==V()&&this.focus?(this.focus(),!1):undefined},delegateType:"focusin"},blur:{trigger:function(){return this===V()&&this.blur?(this.blur(),!1):undefined},delegateType:"focusout"},click:{trigger:function(){return"checkbox"===this.type&&this.click&&x.nodeName(this,"input")?(this.click(),!1):undefined},_default:function(e){return x.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){e.result!==undefined&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=x.extend(new x.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?x.event.trigger(i,null,t):x.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},x.removeEvent=function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)},x.Event=function(e,t){return this instanceof x.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||e.getPreventDefault&&e.getPreventDefault()?U:Y):this.type=e,t&&x.extend(this,t),this.timeStamp=e&&e.timeStamp||x.now(),this[x.expando]=!0,undefined):new x.Event(e,t)},x.Event.prototype={isDefaultPrevented:Y,isPropagationStopped:Y,isImmediatePropagationStopped:Y,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=U,e&&e.preventDefault&&e.preventDefault()},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=U,e&&e.stopPropagation&&e.stopPropagation()},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=U,this.stopPropagation()}},x.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(e,t){x.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!x.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),x.support.focusinBubbles||x.each({focus:"focusin",blur:"focusout"},function(e,t){var n=0,r=function(e){x.event.simulate(t,e.target,x.event.fix(e),!0)};x.event.special[t]={setup:function(){0===n++&&o.addEventListener(e,r,!0)},teardown:function(){0===--n&&o.removeEventListener(e,r,!0)}}}),x.fn.extend({on:function(e,t,n,r,i){var o,s;if("object"==typeof e){"string"!=typeof t&&(n=n||t,t=undefined);for(s in e)this.on(s,t,n,e[s],i);return this}if(null==n&&null==r?(r=t,n=t=undefined):null==r&&("string"==typeof t?(r=n,n=undefined):(r=n,n=t,t=undefined)),r===!1)r=Y;else if(!r)return this;return 1===i&&(o=r,r=function(e){return x().off(e),o.apply(this,arguments)},r.guid=o.guid||(o.guid=x.guid++)),this.each(function(){x.event.add(this,e,r,n,t)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,t,n){var r,i;if(e&&e.preventDefault&&e.handleObj)return r=e.handleObj,x(e.delegateTarget).off(r.namespace?r.origType+"."+r.namespace:r.origType,r.selector,r.handler),this;if("object"==typeof e){for(i in e)this.off(i,t,e[i]);return this}return(t===!1||"function"==typeof t)&&(n=t,t=undefined),n===!1&&(n=Y),this.each(function(){x.event.remove(this,e,n,t)})},trigger:function(e,t){return this.each(function(){x.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];return n?x.event.trigger(e,t,n,!0):undefined}});var G=/^.[^:#\[\.,]*$/,J=x.expr.match.needsContext,Q={children:!0,contents:!0,next:!0,prev:!0};x.fn.extend({find:function(e){var t,n,r,i=this.length;if("string"!=typeof e)return t=this,this.pushStack(x(e).filter(function(){for(r=0;i>r;r++)if(x.contains(t[r],this))return!0}));for(n=[],r=0;i>r;r++)x.find(e,this[r],n);return n=this.pushStack(i>1?x.unique(n):n),n.selector=(this.selector?this.selector+" ":"")+e,n},has:function(e){var t=x(e,this),n=t.length;return this.filter(function(){var e=0;for(;n>e;e++)if(x.contains(this,t[e]))return!0})},not:function(e){return this.pushStack(Z(this,e||[],!0))},filter:function(e){return this.pushStack(Z(this,e||[],!1))},is:function(e){return!!e&&("string"==typeof e?J.test(e)?x(e,this.context).index(this[0])>=0:x.filter(e,this).length>0:this.filter(e).length>0)},closest:function(e,t){var n,r=0,i=this.length,o=[],s=J.test(e)||"string"!=typeof e?x(e,t||this.context):0;for(;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(11>n.nodeType&&(s?s.index(n)>-1:1===n.nodeType&&x.find.matchesSelector(n,e))){n=o.push(n);break}return this.pushStack(o.length>1?x.unique(o):o)},index:function(e){return e?"string"==typeof e?g.call(x(e),this[0]):g.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){var n="string"==typeof e?x(e,t):x.makeArray(e&&e.nodeType?[e]:e),r=x.merge(this.get(),n);return this.pushStack(x.unique(r))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function K(e,t){while((e=e[t])&&1!==e.nodeType);return e}x.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return x.dir(e,"parentNode")},parentsUntil:function(e,t,n){return x.dir(e,"parentNode",n)},next:function(e){return K(e,"nextSibling")},prev:function(e){return K(e,"previousSibling")},nextAll:function(e){return x.dir(e,"nextSibling")},prevAll:function(e){return x.dir(e,"previousSibling")},nextUntil:function(e,t,n){return x.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return x.dir(e,"previousSibling",n)},siblings:function(e){return x.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return x.sibling(e.firstChild)},contents:function(e){return x.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:x.merge([],e.childNodes)}},function(e,t){x.fn[e]=function(n,r){var i=x.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=x.filter(r,i)),this.length>1&&(Q[e]||x.unique(i),"p"===e[0]&&i.reverse()),this.pushStack(i)}}),x.extend({filter:function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?x.find.matchesSelector(r,e)?[r]:[]:x.find.matches(e,x.grep(t,function(e){return 1===e.nodeType}))},dir:function(e,t,n){var r=[],i=n!==undefined;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&x(e).is(n))break;r.push(e)}return r},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}});function Z(e,t,n){if(x.isFunction(t))return x.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return x.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(G.test(t))return x.filter(t,e,n);t=x.filter(t,e)}return x.grep(e,function(e){return g.call(t,e)>=0!==n})}var et=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,tt=/<([\w:]+)/,nt=/<|?\w+;/,rt=/<(?:script|style|link)/i,it=/^(?:checkbox|radio)$/i,ot=/checked\s*(?:[^=]|=\s*.checked.)/i,st=/^$|\/(?:java|ecma)script/i,at=/^true\/(.*)/,ut=/^\s*\s*$/g,lt={option:[1,""],thead:[1,""],tr:[2,""],td:[3,""],_default:[0,"",""]};lt.optgroup=lt.option,lt.tbody=lt.tfoot=lt.colgroup=lt.caption=lt.col=lt.thead,lt.th=lt.td,x.fn.extend({text:function(e){return x.access(this,function(e){return e===undefined?x.text(this):this.empty().append((this[0]&&this[0].ownerDocument||o).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=ct(this,e);t.appendChild(e)}})},prepend:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=ct(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){var n,r=e?x.filter(e,this):this,i=0;for(;null!=(n=r[i]);i++)t||1!==n.nodeType||x.cleanData(gt(n)),n.parentNode&&(t&&x.contains(n.ownerDocument,n)&&ht(gt(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){var e,t=0;for(;null!=(e=this[t]);t++)1===e.nodeType&&(x.cleanData(gt(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return x.clone(this,e,t)})},html:function(e){return x.access(this,function(e){var t=this[0]||{},n=0,r=this.length;if(e===undefined&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!rt.test(e)&&!lt[(tt.exec(e)||["",""])[1].toLowerCase()]){e=e.replace(et,"<$1>$2>");try{for(;r>n;n++)t=this[n]||{},1===t.nodeType&&(x.cleanData(gt(t,!1)),t.innerHTML=e);t=0}catch(i){}}t&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=x.map(this,function(e){return[e.nextSibling,e.parentNode]}),t=0;return this.domManip(arguments,function(n){var r=e[t++],i=e[t++];i&&(x(this).remove(),i.insertBefore(n,r))},!0),t?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t,n){e=p.apply([],e);var r,i,o,s,a,u,l=0,c=this.length,f=this,h=c-1,d=e[0],g=x.isFunction(d);if(g||!(1>=c||"string"!=typeof d||x.support.checkClone)&&ot.test(d))return this.each(function(r){var i=f.eq(r);g&&(e[0]=d.call(this,r,i.html())),i.domManip(e,t,n)});if(c&&(r=x.buildFragment(e,this[0].ownerDocument,!1,!n&&this),i=r.firstChild,1===r.childNodes.length&&(r=i),i)){for(o=x.map(gt(r,"script"),ft),s=o.length;c>l;l++)a=r,l!==h&&(a=x.clone(a,!0,!0),s&&x.merge(o,gt(a,"script"))),t.call(this[l],a,l);if(s)for(u=o[o.length-1].ownerDocument,x.map(o,pt),l=0;s>l;l++)a=o[l],st.test(a.type||"")&&!q.access(a,"globalEval")&&x.contains(u,a)&&(a.src?x._evalUrl(a.src):x.globalEval(a.textContent.replace(ut,"")))}return this}}),x.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){x.fn[e]=function(e){var n,r=[],i=x(e),o=i.length-1,s=0;for(;o>=s;s++)n=s===o?this:this.clone(!0),x(i[s])[t](n),h.apply(r,n.get());return this.pushStack(r)}}),x.extend({clone:function(e,t,n){var r,i,o,s,a=e.cloneNode(!0),u=x.contains(e.ownerDocument,e);if(!(x.support.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||x.isXMLDoc(e)))for(s=gt(a),o=gt(e),r=0,i=o.length;i>r;r++)mt(o[r],s[r]);if(t)if(n)for(o=o||gt(e),s=s||gt(a),r=0,i=o.length;i>r;r++)dt(o[r],s[r]);else dt(e,a);return s=gt(a,"script"),s.length>0&&ht(s,!u&>(e,"script")),a},buildFragment:function(e,t,n,r){var i,o,s,a,u,l,c=0,f=e.length,p=t.createDocumentFragment(),h=[];for(;f>c;c++)if(i=e[c],i||0===i)if("object"===x.type(i))x.merge(h,i.nodeType?[i]:i);else if(nt.test(i)){o=o||p.appendChild(t.createElement("div")),s=(tt.exec(i)||["",""])[1].toLowerCase(),a=lt[s]||lt._default,o.innerHTML=a[1]+i.replace(et,"<$1>$2>")+a[2],l=a[0];while(l--)o=o.firstChild;x.merge(h,o.childNodes),o=p.firstChild,o.textContent=""}else h.push(t.createTextNode(i));p.textContent="",c=0;while(i=h[c++])if((!r||-1===x.inArray(i,r))&&(u=x.contains(i.ownerDocument,i),o=gt(p.appendChild(i),"script"),u&&ht(o),n)){l=0;while(i=o[l++])st.test(i.type||"")&&n.push(i)}return p},cleanData:function(e){var t,n,r,i=e.length,o=0,s=x.event.special;for(;i>o;o++){if(n=e[o],x.acceptData(n)&&(t=q.access(n)))for(r in t.events)s[r]?x.event.remove(n,r):x.removeEvent(n,r,t.handle);L.discard(n),q.discard(n)}},_evalUrl:function(e){return x.ajax({url:e,type:"GET",dataType:"text",async:!1,global:!1,success:x.globalEval})}});function ct(e,t){return x.nodeName(e,"table")&&x.nodeName(1===t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function ft(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function pt(e){var t=at.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function ht(e,t){var n=e.length,r=0;for(;n>r;r++)q.set(e[r],"globalEval",!t||q.get(t[r],"globalEval"))}function dt(e,t){var n,r,i,o,s,a,u,l;if(1===t.nodeType){if(q.hasData(e)&&(o=q.access(e),s=x.extend({},o),l=o.events,q.set(t,s),l)){delete s.handle,s.events={};for(i in l)for(n=0,r=l[i].length;r>n;n++)x.event.add(t,i,l[i][n])}L.hasData(e)&&(a=L.access(e),u=x.extend({},a),L.set(t,u))}}function gt(e,t){var n=e.getElementsByTagName?e.getElementsByTagName(t||"*"):e.querySelectorAll?e.querySelectorAll(t||"*"):[];return t===undefined||t&&x.nodeName(e,t)?x.merge([e],n):n}function mt(e,t){var n=t.nodeName.toLowerCase();"input"===n&&it.test(e.type)?t.checked=e.checked:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}x.fn.extend({wrapAll:function(e){var t;return x.isFunction(e)?this.each(function(t){x(this).wrapAll(e.call(this,t))}):(this[0]&&(t=x(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this)},wrapInner:function(e){return x.isFunction(e)?this.each(function(t){x(this).wrapInner(e.call(this,t))}):this.each(function(){var t=x(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=x.isFunction(e);return this.each(function(n){x(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){x.nodeName(this,"body")||x(this).replaceWith(this.childNodes)}).end()}});var yt,vt,xt=/^(none|table(?!-c[ea]).+)/,bt=/^margin/,wt=RegExp("^("+b+")(.*)$","i"),Tt=RegExp("^("+b+")(?!px)[a-z%]+$","i"),Ct=RegExp("^([+-])=("+b+")","i"),kt={BODY:"block"},Nt={position:"absolute",visibility:"hidden",display:"block"},Et={letterSpacing:0,fontWeight:400},St=["Top","Right","Bottom","Left"],jt=["Webkit","O","Moz","ms"];function Dt(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=jt.length;while(i--)if(t=jt[i]+n,t in e)return t;return r}function At(e,t){return e=t||e,"none"===x.css(e,"display")||!x.contains(e.ownerDocument,e)}function Lt(t){return e.getComputedStyle(t,null)}function qt(e,t){var n,r,i,o=[],s=0,a=e.length;for(;a>s;s++)r=e[s],r.style&&(o[s]=q.get(r,"olddisplay"),n=r.style.display,t?(o[s]||"none"!==n||(r.style.display=""),""===r.style.display&&At(r)&&(o[s]=q.access(r,"olddisplay",Pt(r.nodeName)))):o[s]||(i=At(r),(n&&"none"!==n||!i)&&q.set(r,"olddisplay",i?n:x.css(r,"display"))));for(s=0;a>s;s++)r=e[s],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[s]||"":"none"));return e}x.fn.extend({css:function(e,t){return x.access(this,function(e,t,n){var r,i,o={},s=0;if(x.isArray(t)){for(r=Lt(e),i=t.length;i>s;s++)o[t[s]]=x.css(e,t[s],!1,r);return o}return n!==undefined?x.style(e,t,n):x.css(e,t)},e,t,arguments.length>1)},show:function(){return qt(this,!0)},hide:function(){return qt(this)},toggle:function(e){var t="boolean"==typeof e;return this.each(function(){(t?e:At(this))?x(this).show():x(this).hide()})}}),x.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=yt(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,s,a=x.camelCase(t),u=e.style;return t=x.cssProps[a]||(x.cssProps[a]=Dt(u,a)),s=x.cssHooks[t]||x.cssHooks[a],n===undefined?s&&"get"in s&&(i=s.get(e,!1,r))!==undefined?i:u[t]:(o=typeof n,"string"===o&&(i=Ct.exec(n))&&(n=(i[1]+1)*i[2]+parseFloat(x.css(e,t)),o="number"),null==n||"number"===o&&isNaN(n)||("number"!==o||x.cssNumber[a]||(n+="px"),x.support.clearCloneStyle||""!==n||0!==t.indexOf("background")||(u[t]="inherit"),s&&"set"in s&&(n=s.set(e,n,r))===undefined||(u[t]=n)),undefined)}},css:function(e,t,n,r){var i,o,s,a=x.camelCase(t);return t=x.cssProps[a]||(x.cssProps[a]=Dt(e.style,a)),s=x.cssHooks[t]||x.cssHooks[a],s&&"get"in s&&(i=s.get(e,!0,n)),i===undefined&&(i=yt(e,t,r)),"normal"===i&&t in Et&&(i=Et[t]),""===n||n?(o=parseFloat(i),n===!0||x.isNumeric(o)?o||0:i):i}}),yt=function(e,t,n){var r,i,o,s=n||Lt(e),a=s?s.getPropertyValue(t)||s[t]:undefined,u=e.style;return s&&(""!==a||x.contains(e.ownerDocument,e)||(a=x.style(e,t)),Tt.test(a)&&bt.test(t)&&(r=u.width,i=u.minWidth,o=u.maxWidth,u.minWidth=u.maxWidth=u.width=a,a=s.width,u.width=r,u.minWidth=i,u.maxWidth=o)),a};function Ht(e,t,n){var r=wt.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function Ot(e,t,n,r,i){var o=n===(r?"border":"content")?4:"width"===t?1:0,s=0;for(;4>o;o+=2)"margin"===n&&(s+=x.css(e,n+St[o],!0,i)),r?("content"===n&&(s-=x.css(e,"padding"+St[o],!0,i)),"margin"!==n&&(s-=x.css(e,"border"+St[o]+"Width",!0,i))):(s+=x.css(e,"padding"+St[o],!0,i),"padding"!==n&&(s+=x.css(e,"border"+St[o]+"Width",!0,i)));return s}function Ft(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=Lt(e),s=x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=yt(e,t,o),(0>i||null==i)&&(i=e.style[t]),Tt.test(i))return i;r=s&&(x.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+Ot(e,t,n||(s?"border":"content"),r,o)+"px"}function Pt(e){var t=o,n=kt[e];return n||(n=Rt(e,t),"none"!==n&&n||(vt=(vt||x("").css("cssText","display:block !important")).appendTo(t.documentElement),t=(vt[0].contentWindow||vt[0].contentDocument).document,t.write(""),t.close(),n=Rt(e,t),vt.detach()),kt[e]=n),n}function Rt(e,t){var n=x(t.createElement(e)).appendTo(t.body),r=x.css(n[0],"display");return n.remove(),r}x.each(["height","width"],function(e,t){x.cssHooks[t]={get:function(e,n,r){return n?0===e.offsetWidth&&xt.test(x.css(e,"display"))?x.swap(e,Nt,function(){return Ft(e,t,r)}):Ft(e,t,r):undefined},set:function(e,n,r){var i=r&&Lt(e);return Ht(e,n,r?Ot(e,t,r,x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,i),i):0)}}}),x(function(){x.support.reliableMarginRight||(x.cssHooks.marginRight={get:function(e,t){return t?x.swap(e,{display:"inline-block"},yt,[e,"marginRight"]):undefined}}),!x.support.pixelPosition&&x.fn.position&&x.each(["top","left"],function(e,t){x.cssHooks[t]={get:function(e,n){return n?(n=yt(e,t),Tt.test(n)?x(e).position()[t]+"px":n):undefined}}})}),x.expr&&x.expr.filters&&(x.expr.filters.hidden=function(e){return 0>=e.offsetWidth&&0>=e.offsetHeight},x.expr.filters.visible=function(e){return!x.expr.filters.hidden(e)}),x.each({margin:"",padding:"",border:"Width"},function(e,t){x.cssHooks[e+t]={expand:function(n){var r=0,i={},o="string"==typeof n?n.split(" "):[n];for(;4>r;r++)i[e+St[r]+t]=o[r]||o[r-2]||o[0];return i}},bt.test(e)||(x.cssHooks[e+t].set=Ht)});var Mt=/%20/g,Wt=/\[\]$/,$t=/\r?\n/g,Bt=/^(?:submit|button|image|reset|file)$/i,It=/^(?:input|select|textarea|keygen)/i;x.fn.extend({serialize:function(){return x.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=x.prop(this,"elements");return e?x.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!x(this).is(":disabled")&&It.test(this.nodeName)&&!Bt.test(e)&&(this.checked||!it.test(e))}).map(function(e,t){var n=x(this).val();return null==n?null:x.isArray(n)?x.map(n,function(e){return{name:t.name,value:e.replace($t,"\r\n")}}):{name:t.name,value:n.replace($t,"\r\n")}}).get()}}),x.param=function(e,t){var n,r=[],i=function(e,t){t=x.isFunction(t)?t():null==t?"":t,r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};if(t===undefined&&(t=x.ajaxSettings&&x.ajaxSettings.traditional),x.isArray(e)||e.jquery&&!x.isPlainObject(e))x.each(e,function(){i(this.name,this.value)});else for(n in e)zt(n,e[n],t,i);return r.join("&").replace(Mt,"+")};function zt(e,t,n,r){var i;if(x.isArray(t))x.each(t,function(t,i){n||Wt.test(e)?r(e,i):zt(e+"["+("object"==typeof i?t:"")+"]",i,n,r)});else if(n||"object"!==x.type(t))r(e,t);else for(i in t)zt(e+"["+i+"]",t[i],n,r)}x.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(e,t){x.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),x.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)},bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)}});var _t,Xt,Ut=x.now(),Yt=/\?/,Vt=/#.*$/,Gt=/([?&])_=[^&]*/,Jt=/^(.*?):[ \t]*([^\r\n]*)$/gm,Qt=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Kt=/^(?:GET|HEAD)$/,Zt=/^\/\//,en=/^([\w.+-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,tn=x.fn.load,nn={},rn={},on="*/".concat("*");try{Xt=i.href}catch(sn){Xt=o.createElement("a"),Xt.href="",Xt=Xt.href}_t=en.exec(Xt.toLowerCase())||[];function an(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(w)||[];
+if(x.isFunction(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function un(e,t,n,r){var i={},o=e===rn;function s(a){var u;return i[a]=!0,x.each(e[a]||[],function(e,a){var l=a(t,n,r);return"string"!=typeof l||o||i[l]?o?!(u=l):undefined:(t.dataTypes.unshift(l),s(l),!1)}),u}return s(t.dataTypes[0])||!i["*"]&&s("*")}function ln(e,t){var n,r,i=x.ajaxSettings.flatOptions||{};for(n in t)t[n]!==undefined&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&x.extend(!0,e,r),e}x.fn.load=function(e,t,n){if("string"!=typeof e&&tn)return tn.apply(this,arguments);var r,i,o,s=this,a=e.indexOf(" ");return a>=0&&(r=e.slice(a),e=e.slice(0,a)),x.isFunction(t)?(n=t,t=undefined):t&&"object"==typeof t&&(i="POST"),s.length>0&&x.ajax({url:e,type:i,dataType:"html",data:t}).done(function(e){o=arguments,s.html(r?x("").append(x.parseHTML(e)).find(r):e)}).complete(n&&function(e,t){s.each(n,o||[e.responseText,t,e])}),this},x.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){x.fn[t]=function(e){return this.on(t,e)}}),x.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Xt,type:"GET",isLocal:Qt.test(_t[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":on,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":x.parseJSON,"text xml":x.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?ln(ln(e,x.ajaxSettings),t):ln(x.ajaxSettings,e)},ajaxPrefilter:an(nn),ajaxTransport:an(rn),ajax:function(e,t){"object"==typeof e&&(t=e,e=undefined),t=t||{};var n,r,i,o,s,a,u,l,c=x.ajaxSetup({},t),f=c.context||c,p=c.context&&(f.nodeType||f.jquery)?x(f):x.event,h=x.Deferred(),d=x.Callbacks("once memory"),g=c.statusCode||{},m={},y={},v=0,b="canceled",T={readyState:0,getResponseHeader:function(e){var t;if(2===v){if(!o){o={};while(t=Jt.exec(i))o[t[1].toLowerCase()]=t[2]}t=o[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return 2===v?i:null},setRequestHeader:function(e,t){var n=e.toLowerCase();return v||(e=y[n]=y[n]||e,m[e]=t),this},overrideMimeType:function(e){return v||(c.mimeType=e),this},statusCode:function(e){var t;if(e)if(2>v)for(t in e)g[t]=[g[t],e[t]];else T.always(e[T.status]);return this},abort:function(e){var t=e||b;return n&&n.abort(t),k(0,t),this}};if(h.promise(T).complete=d.add,T.success=T.done,T.error=T.fail,c.url=((e||c.url||Xt)+"").replace(Vt,"").replace(Zt,_t[1]+"//"),c.type=t.method||t.type||c.method||c.type,c.dataTypes=x.trim(c.dataType||"*").toLowerCase().match(w)||[""],null==c.crossDomain&&(a=en.exec(c.url.toLowerCase()),c.crossDomain=!(!a||a[1]===_t[1]&&a[2]===_t[2]&&(a[3]||("http:"===a[1]?"80":"443"))===(_t[3]||("http:"===_t[1]?"80":"443")))),c.data&&c.processData&&"string"!=typeof c.data&&(c.data=x.param(c.data,c.traditional)),un(nn,c,t,T),2===v)return T;u=c.global,u&&0===x.active++&&x.event.trigger("ajaxStart"),c.type=c.type.toUpperCase(),c.hasContent=!Kt.test(c.type),r=c.url,c.hasContent||(c.data&&(r=c.url+=(Yt.test(r)?"&":"?")+c.data,delete c.data),c.cache===!1&&(c.url=Gt.test(r)?r.replace(Gt,"$1_="+Ut++):r+(Yt.test(r)?"&":"?")+"_="+Ut++)),c.ifModified&&(x.lastModified[r]&&T.setRequestHeader("If-Modified-Since",x.lastModified[r]),x.etag[r]&&T.setRequestHeader("If-None-Match",x.etag[r])),(c.data&&c.hasContent&&c.contentType!==!1||t.contentType)&&T.setRequestHeader("Content-Type",c.contentType),T.setRequestHeader("Accept",c.dataTypes[0]&&c.accepts[c.dataTypes[0]]?c.accepts[c.dataTypes[0]]+("*"!==c.dataTypes[0]?", "+on+"; q=0.01":""):c.accepts["*"]);for(l in c.headers)T.setRequestHeader(l,c.headers[l]);if(c.beforeSend&&(c.beforeSend.call(f,T,c)===!1||2===v))return T.abort();b="abort";for(l in{success:1,error:1,complete:1})T[l](c[l]);if(n=un(rn,c,t,T)){T.readyState=1,u&&p.trigger("ajaxSend",[T,c]),c.async&&c.timeout>0&&(s=setTimeout(function(){T.abort("timeout")},c.timeout));try{v=1,n.send(m,k)}catch(C){if(!(2>v))throw C;k(-1,C)}}else k(-1,"No Transport");function k(e,t,o,a){var l,m,y,b,w,C=t;2!==v&&(v=2,s&&clearTimeout(s),n=undefined,i=a||"",T.readyState=e>0?4:0,l=e>=200&&300>e||304===e,o&&(b=cn(c,T,o)),b=fn(c,b,T,l),l?(c.ifModified&&(w=T.getResponseHeader("Last-Modified"),w&&(x.lastModified[r]=w),w=T.getResponseHeader("etag"),w&&(x.etag[r]=w)),204===e?C="nocontent":304===e?C="notmodified":(C=b.state,m=b.data,y=b.error,l=!y)):(y=C,(e||!C)&&(C="error",0>e&&(e=0))),T.status=e,T.statusText=(t||C)+"",l?h.resolveWith(f,[m,C,T]):h.rejectWith(f,[T,C,y]),T.statusCode(g),g=undefined,u&&p.trigger(l?"ajaxSuccess":"ajaxError",[T,c,l?m:y]),d.fireWith(f,[T,C]),u&&(p.trigger("ajaxComplete",[T,c]),--x.active||x.event.trigger("ajaxStop")))}return T},getJSON:function(e,t,n){return x.get(e,t,n,"json")},getScript:function(e,t){return x.get(e,undefined,t,"script")}}),x.each(["get","post"],function(e,t){x[t]=function(e,n,r,i){return x.isFunction(n)&&(i=i||r,r=n,n=undefined),x.ajax({url:e,type:t,dataType:i,data:n,success:r})}});function cn(e,t,n){var r,i,o,s,a=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),r===undefined&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(i in a)if(a[i]&&a[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+" "+u[0]]){o=i;break}s||(s=i)}o=o||s}return o?(o!==u[0]&&u.unshift(o),n[o]):undefined}function fn(e,t,n,r){var i,o,s,a,u,l={},c=e.dataTypes.slice();if(c[1])for(s in e.converters)l[s.toLowerCase()]=e.converters[s];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(s=l[u+" "+o]||l["* "+o],!s)for(i in l)if(a=i.split(" "),a[1]===o&&(s=l[u+" "+a[0]]||l["* "+a[0]])){s===!0?s=l[i]:l[i]!==!0&&(o=a[0],c.unshift(a[1]));break}if(s!==!0)if(s&&e["throws"])t=s(t);else try{t=s(t)}catch(f){return{state:"parsererror",error:s?f:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}x.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(e){return x.globalEval(e),e}}}),x.ajaxPrefilter("script",function(e){e.cache===undefined&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),x.ajaxTransport("script",function(e){if(e.crossDomain){var t,n;return{send:function(r,i){t=x("
-
+
@@ -107,16 +113,17 @@
Machine State:
- File:
+ File:
(SD)
Filament:
Estimated Print Time:
- Line:
- Height:
+ Timelapse:
+ Height:
Print Time:
Print Time Left:
+ Printed:
@@ -131,7 +138,7 @@
Files
-
+
+
+ {% if enableSdSupport %}
+
+ {% endif %}
@@ -155,15 +180,18 @@
-
+
|
|
- | |
+ | |
|
+
+ Free:
+
-
-
- Upload
-
-
+
+ {% if enableSdSupport %}
+
+
+ Upload
+
+
+
+
+ Upload to SD
+
+
+ {% else %}
+
+
+ Upload
+
+
+ {% endif %}
+
@@ -204,25 +247,27 @@
+ {% if enableTemperatureGraph %}
+ {% endif %}
{% if webcamStream %}
-
+
{% endif %}
@@ -356,6 +417,16 @@
+
+
+ {% if enableGCodeVisualizer %}
+ {% endif %}
{% if enableTimelapse %}
@@ -481,13 +559,19 @@
+
+
+
+
-
+
@@ -508,7 +592,7 @@
|
|
- | |
+ | |
@@ -529,7 +613,12 @@