Conflicts:
	octoprint/users.py
master
q3k 2013-10-06 02:11:17 +02:00
commit 627aad1954
72 changed files with 6421 additions and 7223 deletions

23
.gitignore vendored
View File

@ -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

2
MANIFEST.in Normal file
View File

@ -0,0 +1,2 @@
recursive-include octoprint/static *
recursive-include octoprint/templates *

View File

@ -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).

246
octoprint/events.py Normal file
View File

@ -0,0 +1,246 @@
# coding=utf-8
__author__ = "Lars Norpchen"
__author__ = "Gina Häußge <osd@foosel.net>"
__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(","))

View File

@ -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)))

View File

@ -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)
}

File diff suppressed because it is too large Load Diff

View File

@ -2,11 +2,12 @@
__author__ = "Gina Häußge <osd@foosel.net>"
__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)

View File

@ -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;
}

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 780 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,123 @@
function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewModel, temperatureViewModel, controlViewModel, terminalViewModel, gcodeFilesViewModel, timelapseViewModel, gcodeViewModel) {
var self = this;
self.loginStateViewModel = loginStateViewModel;
self.connectionViewModel = connectionViewModel;
self.printerStateViewModel = printerStateViewModel;
self.temperatureViewModel = temperatureViewModel;
self.controlViewModel = controlViewModel;
self.terminalViewModel = terminalViewModel;
self.gcodeFilesViewModel = gcodeFilesViewModel;
self.timelapseViewModel = timelapseViewModel;
self.gcodeViewModel = gcodeViewModel;
self._socket = undefined;
self._autoReconnecting = false;
self._autoReconnectTrial = 0;
self._autoReconnectTimeouts = [1, 1, 2, 3, 5, 8, 13, 20, 40, 100];
self.connect = function() {
var options = {};
if (SOCKJS_DEBUG) {
options["debug"] = true;
}
self._socket = new SockJS(SOCKJS_URI, undefined, options);
self._socket.onopen = self._onconnect;
self._socket.onclose = self._onclose;
self._socket.onmessage = self._onmessage;
}
self.reconnect = function() {
delete self._socket;
self.connect();
}
self._onconnect = function() {
self._autoReconnecting = false;
self._autoReconnectTrial = 0;
if ($("#offline_overlay").is(":visible")) {
$("#offline_overlay").hide();
self.timelapseViewModel.requestData();
$("#webcam_image").attr("src", CONFIG_WEBCAM_STREAM + "?" + new Date().getTime());
self.loginStateViewModel.requestData();
self.gcodeFilesViewModel.requestData();
}
}
self._onclose = function() {
$("#offline_overlay_message").html(
"The server appears to be offline, at least I'm not getting any response from it. I'll try to reconnect " +
"automatically <strong>over the next couple of minutes</strong>, 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 <strong>could not reconnect automatically</strong>, " +
"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();
}

View File

@ -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();
}

View File

@ -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: "<p>Could not upload the file. Make sure it is a GCODE file and has one of the following extensions: .gcode, .gco</p><p>Server reported: <pre>" + data.jqXHR.responseText + "</pre></p>",
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();
}
}
);

View File

@ -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";
})
}

View File

@ -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"}
})
}
}
}

View File

@ -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";
}
}
}

View File

@ -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 <strong>and</strong> your OctoPrint " +
"installation is accessible from the internet, your printer <strong>will be accessible by everyone - " +
"that also includes the bad guys!</strong>");
$("#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");
}
}

View File

@ -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);
}
}
}

View File

@ -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 = "<p><strong>Uploaded:</strong> " + data["date"] + "</p>";
if (data["gcodeAnalysis"]) {
output += "<p>";
output += "<strong>Filament:</strong> " + data["gcodeAnalysis"]["filament"] + "<br>";
output += "<strong>Estimated Print Time:</strong> " + data["gcodeAnalysis"]["estimatedPrintTime"];
output += "</p>";
}
if (data["prints"] && data["prints"]["last"]) {
output += "<p>";
output += "<strong>Last Print:</strong> <span class=\"" + (data["prints"]["last"]["success"] ? "text-success" : "text-error") + "\">" + data["prints"]["last"]["date"] + "</span>";
output += "</p>";
}
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);
}
}

View File

@ -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);
}
})
}
}

View File

@ -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: "<p>The command \"" + action.name + "\" could not be executed.</p><p>Reason: <pre>" + jqXHR.responseText + "</pre></p>", 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();
}
}
}

View File

@ -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}
});
}
}

View File

@ -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");
}
})
}
}

View File

@ -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() + " &deg;C";
});
self.bedTempString = ko.computed(function() {
if (!self.bedTemp())
return "-";
return self.bedTemp() + " &deg;C";
});
self.targetTempString = ko.computed(function() {
if (!self.targetTemp())
return "-";
return self.targetTemp() + " &deg;C";
});
self.bedTargetTempString = ko.computed(function() {
if (!self.bedTargetTemp())
return "-";
return self.bedTargetTemp() + " &deg;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();
}
}
}
}

View File

@ -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();
}
}
}

View File

@ -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
});
}
}

View File

@ -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
});
}
}

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

@ -1,4 +1,4 @@
<div id="offline_overlay">
<div id="offline_overlay" xmlns="http://www.w3.org/1999/html">
<div id="offline_overlay_background"></div>
<div id="offline_overlay_wrapper">
<div class="container">
@ -17,6 +17,21 @@
</div>
</div>
<div id="drop_overlay" data-bind="visible: loginState.isUser()">
<div id="drop_overlay_background"></div>
<div id="drop_overlay_wrapper">
{% if enableSdSupport %}
<div class="dropzone" id="drop_locally"><span class="centered"><i class="icon-upload-alt"></i><br>Upload locally</span></div>
<div class="dropzone_background" id="drop_locally_background"></div>
<div class="dropzone" id="drop_sd"><span class="centered"><i class="icon-upload-alt"></i><br>Upload to SD<br><small data-bind="visible: !isSdReady()">(SD not initialized)</small></span></div>
<div class="dropzone_background" id="drop_sd_background"></div>
{% else %}
<div class="dropzone" id="drop"><span class="centered"><i class="icon-upload-alt"></i><br>Upload</span></div>
<div class="dropzone_background" id="drop_background"></div>
{% endif %}
</div>
</div>
<div id="confirmation_dialog" class="modal hide fade">
<div class="modal-header">
<a href="#" class="close" data-dismiss="modal" aria-hidden="true">&times;</a>
@ -30,4 +45,59 @@
<a href="#" class="btn" data-dismiss="modal" aria-hidden="true">Cancel</a>
<a href="#" class="btn btn-danger confirmation_dialog_acknowledge">Proceed</a>
</div>
</div>
<div id="first_run_dialog" class="modal hide fade" data-backdrop="static" data-keyboard="false">
<div class="modal-header">
<h3><i class="icon-warning-sign"></i> Configure Access Control</h3>
</div>
<div class="modal-body">
<p>
<strong>Please read the following, it is very important for your printer's health!</strong>
</p>
<p>
OctoPrint by default now ships with Access Control enabled, meaning you won't be able to do anything with the
printer unless you login first as a configured user. This is to <strong>prevent strangers - possibly with
malicious intent - to gain access to your printer</strong> via the internet or another untrustworthy network
and using it in such a way that it is damaged or worse (i.e. causes a fire).
</p>
<p>
It looks like you haven't configured access control yet. Please <strong>set up an username and password</strong> for the
initial administrator account who will have full access to both the printer and OctoPrint's settings, then click
on "Keep Access Control Enabled":
</p>
<form class="form-horizontal">
<div class="control-group" data-bind="css: {success: validUsername()}">
<label class="control-label" for="first_run_username">Username</label>
<div class="controls">
<input type="text" class="input-medium" data-bind="value: username, valueUpdate: 'afterkeydown'">
</div>
</div>
<div class="control-group" data-bind="css: {success: validPassword()}">
<label class="control-label" for="first_run_username">Password</label>
<div class="controls">
<input type="password" class="input-medium" data-bind="value: password, valueUpdate: 'afterkeydown'">
</div>
</div>
<div class="control-group" data-bind="css: {error: passwordMismatch(), success: validPassword() && !passwordMismatch()}">
<label class="control-label" for="first_run_username">Confirm Password</label>
<div class="controls">
<input type="password" class="input-medium" data-bind="value: confirmedPassword, valueUpdate: 'afterkeydown'">
<span class="help-inline" data-bind="visible: passwordMismatch()">Passwords don't match</span>
</div>
</div>
</form>
<p>
<strong>Note:</strong> In case that your OctoPrint installation is only accessible from within a trustworthy network and you don't
need Access Control for other reasons, you may alternatively disable Access Control. You should only
do this if you are absolutely certain that only people you know and trust will be able to connect to it.
</p>
<p>
<strong>Do NOT underestimate the risk of an unsecured access from the internet to your printer!</strong>
</p>
</div>
<div class="modal-footer">
<a href="#" class="btn btn-danger" data-bind="click: disableAccessControl">Disable Access Control</a>
<a href="#" class="btn btn-primary" data-bind="click: keepAccessControl, enable: validData(), css: {disabled: !validData()}">Keep Access Control Enabled</a>
</div>
</div>

View File

@ -24,17 +24,20 @@
var CONFIG_USERSPERPAGE = 10;
var CONFIG_WEBCAM_STREAM = "{{ webcamStream }}";
var CONFIG_ACCESS_CONTROL = {% if enableAccessControl -%} true; {% else %} false; {%- endif %}
var CONFIG_SD_SUPPORT = {% if enableSdSupport -%} true; {% else %} false; {%- endif %}
var CONFIG_FIRST_RUN = {% if firstRun -%} true; {% else %} false; {%- endif %}
var CONFIG_TEMPERATURE_GRAPH = {% if enableTemperatureGraph -%} true; {% else %} false; {%- endif %}
var WEB_SOCKET_SWF_LOCATION = "{{ url_for('static', filename='js/socket.io/WebSocketMain.swf') }}";
var WEB_SOCKET_DEBUG = true;
var SOCKJS_URI = window.location.protocol.slice(0, -1) + "://" + (window.document ? window.document.domain : window.location.hostname) + ":" + window.location.port + "/sockjs";
var SOCKJS_DEBUG = {% if debug -%} true; {% else %} false; {%- endif %}
</script>
<script src="{{ url_for('static', filename='js/less-1.3.3.min.js') }}" type="text/javascript"></script>
<script src="{{ url_for('static', filename='js/lib/less-1.3.3.min.js') }}" type="text/javascript"></script>
</head>
<body>
<div id="navbar" class="navbar navbar-fixed-top">
<div class="navbar-inner" data-bind="css: appearance.color">
<div class="container">
<a class="brand" href="#"><img src="{{ url_for('static', filename='img/tentacle-20x20.png') }}"> <span data-bind="text: appearance.brand">OctoPrint</span></a>
<a class="brand" href="#"> <span data-bind="text: appearance.brand">OctoPrint</span></a>
<div class="nav-collapse">
<ul class="nav pull-right">
<li style="display: none;" data-bind="visible: loginState.isAdmin">
@ -89,14 +92,17 @@
</div>
<div class="accordion-body collapse in" id="connection">
<div class="accordion-inner">
<label for="connection_ports" data-bind="css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed && loginState.isUser">Serial Port</label>
<select id="connection_ports" data-bind="options: portOptions, optionsCaption: 'AUTO', value: selectedPort, css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed && loginState.isUser"></select>
<label for="connection_baudrates" data-bind="css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed && loginState.isUser">Baudrate</label>
<select id="connection_baudrates" data-bind="options: baudrateOptions, optionsCaption: 'AUTO', value: selectedBaudrate, css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed && loginState.isUser"></select>
<label for="connection_ports" data-bind="css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()">Serial Port</label>
<select id="connection_ports" data-bind="options: portOptions, optionsCaption: 'AUTO', value: selectedPort, css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()"></select>
<label for="connection_baudrates" data-bind="css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()">Baudrate</label>
<select id="connection_baudrates" data-bind="options: baudrateOptions, optionsCaption: 'AUTO', value: selectedBaudrate, css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()"></select>
<label class="checkbox">
<input type="checkbox" id="connection_save" data-bind="checked: saveSettings, css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed && loginState.isUser"> Save connection settings
<input type="checkbox" id="connection_save" data-bind="checked: saveSettings, css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()"> Save connection settings
</label>
<button class="btn btn-block" id="printer_connect" data-bind="click: connect, text: buttonText(), enable: loginState.isUser">Connect</button>
<label class="checkbox">
<input type="checkbox" id="connection_autoconnect" data-bind="checked: settings.serial_autoconnect, css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()"> Auto-connect on server startup
</label>
<button class="btn btn-block" id="printer_connect" data-bind="click: connect, text: buttonText(), enable: loginState.isUser()">Connect</button>
</div>
</div>
</div>
@ -107,16 +113,17 @@
<div class="accordion-body collapse in" id="state">
<div class="accordion-inner">
Machine State: <strong data-bind="text: stateString"></strong><br>
File: <strong data-bind="text: filename"></strong><br>
File: <strong data-bind="text: filename"></strong>&nbsp;<strong data-bind="visible: sd">(SD)</strong><br>
Filament: <strong data-bind="text: filament"></strong><br>
Estimated Print Time: <strong data-bind="text: estimatedPrintTime"></strong><br>
Line: <strong data-bind="text: lineString"></strong><br>
Height: <strong data-bind="text: currentHeight"></strong><br>
Timelapse: <strong data-bind="text: timelapseString"></strong><br>
Height: <strong data-bind="text: heightString"></strong><br>
Print Time: <strong data-bind="text: printTime"></strong><br>
Print Time Left: <strong data-bind="text: printTimeLeft"></strong><br>
Printed: <strong data-bind="text: byteString"></strong><br>
<div class="progress">
<div class="bar" id="job_progressBar" data-bind="style: { width: progress() + '%' }"></div>
<div class="bar" id="job_progressBar" data-bind="style: { width: progressString() + '%' }"></div>
</div>
<div class="row-fluid print-control" style="display: none;" data-bind="visible: loginState.isUser">
@ -131,7 +138,7 @@
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#files"><i class="icon-list"></i> Files</a>
<div class="settings-trigger btn-group">
<div class="settings-trigger accordion-heading-button btn-group">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<span class="icon-wrench"></span>
</a>
@ -139,10 +146,28 @@
<li><a href="#" data-bind="click: function() { $root.listHelper.changeSorting('name'); }"><i class="icon-ok" data-bind="style: {visibility: listHelper.currentSorting() == 'name' ? 'visible' : 'hidden'}"></i> Sort by name (ascending)</a></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.changeSorting('upload'); }"><i class="icon-ok" data-bind="style: {visibility: listHelper.currentSorting() == 'upload' ? 'visible' : 'hidden'}"></i> Sort by upload date (descending)</a></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.changeSorting('size'); }"><i class="icon-ok" data-bind="style: {visibility: listHelper.currentSorting() == 'size' ? 'visible' : 'hidden'}"></i> Sort by file size (descending)</a></li>
{% if enableSdSupport %}
<li class="divider"></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.toggleFilter('local'); }"><i class="icon-ok" data-bind="style: {visibility: _.contains(listHelper.currentFilters(), 'local') ? 'visible' : 'hidden'}"></i> Only show files stored locally</a></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.toggleFilter('sd'); }"><i class="icon-ok" data-bind="style: {visibility: _.contains(listHelper.currentFilters(), 'sd') ? 'visible' : 'hidden'}"></i> Only show files stored on SD</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.toggleFilter('printed'); }"><i class="icon-ok" data-bind="style: {visibility: _.contains(listHelper.currentFilters(), 'printed') ? 'visible' : 'hidden'}"></i> Hide successfully printed files</a></li>
</ul>
</div>
{% if enableSdSupport %}
<div class="sd-trigger accordion-heading-button btn-group">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<span class="icon-sd-black-14"></span>
</a>
<ul class="dropdown-menu">
<li data-bind="visible: !isSdReady()"><a href="#" data-bind="click: function() { $root.initSdCard(); }"><i class="icon-flag"></i> Initialize SD card</a></li>
<li data-bind="visible: isSdReady()"><a href="#" data-bind="click: function() { $root.refreshSdFiles(); }"><i class="icon-refresh"></i> Refresh SD files</a></li>
<li data-bind="visible: isSdReady()"><a href="#" data-bind="click: function() { $root.releaseSdCard(); }"><i class="icon-eject"></i> Release SD card</a></li>
</ul>
</div>
{% endif %}
</div>
<div class="accordion-body collapse in overflow_visible" id="files">
<div class="accordion-inner">
@ -155,15 +180,18 @@
</tr>
</thead>
<tbody data-bind="foreach: listHelper.paginatedItems">
<tr data-bind="css: $root.getSuccessClass($data), popover: { title: name, animation: true, html: true, placement: 'right', trigger: 'hover', delay: 0, content: $root.getPopoverContent($data), html: true }">
<tr data-bind="css: $root.getSuccessClass($data), style: { 'font-weight': $root.listHelper.isSelected($data) ? 'bold' : 'normal' }, popover: { title: name, animation: true, html: true, placement: 'right', trigger: 'hover', delay: 0, content: $root.getPopoverContent($data), html: true }">
<td class="gcode_files_name" data-bind="text: name"></td>
<td class="gcode_files_size" data-bind="text: size"></td>
<td class="gcode_files_action">
<a href="#" class="icon-trash" title="Remove" data-bind="click: function() { if ($root.loginState.isUser()) { $root.removeFile($data.name); } else { return; } }, css: {disabled: !$root.loginState.isUser()}"></a>&nbsp;|&nbsp;<a href="#" class="icon-folder-open" title="Load" data-bind="click: function() { if ($root.isLoadActionPossible()) { $root.loadFile($data.name, false); } else { return; } }, css: {disabled: !$root.isLoadActionPossible()}"></a>&nbsp;|&nbsp;<a href="#" class="icon-print" title="Load and Print" data-bind="click: function() { if ($root.isLoadAndPrintActionPossible()) { $root.loadFile($data.name, true); } else { return; } }, css: {disabled: !$root.isLoadAndPrintActionPossible()}"></a>
<a href="#" class="icon-trash" title="Remove" data-bind="click: function() { if ($root.enableRemove($data)) { $root.removeFile($data.name); } else { return; } }, css: {disabled: !$root.enableRemove($data)}"></a>&nbsp;|&nbsp;<a href="#" class="icon-folder-open" title="Load" data-bind="click: function() { if ($root.enableSelect($data)) { $root.loadFile($data.name, false); } else { return; } }, css: {disabled: !$root.enableSelect($data)}"></a>&nbsp;|&nbsp;<a href="#" class="icon-print" title="Load and Print" data-bind="click: function() { if ($root.enableSelect($data)) { $root.loadFile($data.name, true); } else { return; } }, css: {disabled: !$root.enableSelect($data)}"></a>
</td>
</tr>
</tbody>
</table>
<div class="muted text-right">
<small>Free: <span data-bind="text: freeSpace"></span></small>
</div>
<div class="pagination pagination-mini pagination-centered">
<ul>
<li data-bind="css: {disabled: listHelper.currentPage() === 0}"><a href="#" data-bind="click: listHelper.prevPage">«</a></li>
@ -176,11 +204,26 @@
</ul>
</div>
<div style="display: none;" data-bind="visible: loginState.isUser">
<span class="btn btn-primary btn-block fileinput-button" data-bind="css: {disabled: !$root.loginState.isUser()}" style="margin-bottom: 10px">
<i class="icon-upload icon-white"></i>
<span>Upload</span>
<input id="gcode_upload" type="file" name="gcode_file" class="fileinput-button" data-url="/ajax/gcodefiles/upload" data-bind="enable: loginState.isUser()">
</span>
<div class="row-fluid upload-buttons">
{% if enableSdSupport %}
<span class="btn btn-primary fileinput-button span6" data-bind="css: {disabled: !$root.loginState.isUser()}" style="margin-bottom: 10px">
<i class="icon-upload-alt icon-white"></i>
<span>Upload</span>
<input id="gcode_upload" type="file" name="gcode_file" class="fileinput-button" data-url="/ajax/gcodefiles/upload" data-bind="enable: loginState.isUser()">
</span>
<span class="btn btn-primary fileinput-button span6" data-bind="css: {disabled: !$root.loginState.isUser() || !$root.isSdReady()}" style="margin-bottom: 10px">
<i class="icon-upload-alt icon-white"></i>
<span>Upload to SD</span>
<input id="gcode_upload_sd" type="file" name="gcode_file" class="fileinput-button" data-url="/ajax/gcodefiles/upload" data-bind="enable: loginState.isUser() && isSdReady()">
</span>
{% else %}
<span class="btn btn-primary fileinput-button span12" data-bind="css: {disabled: !$root.loginState.isUser()}" style="margin-bottom: 10px">
<i class="icon-upload-alt icon-white"></i>
<span>Upload</span>
<input id="gcode_upload" type="file" name="gcode_file" class="fileinput-button" data-url="/ajax/gcodefiles/upload" data-bind="enable: loginState.isUser()">
</span>
{% endif %}
</div>
<div id="gcode_upload_progress" class="progress" style="width: 100%;">
<div class="bar" style="width: 0%"></div>
</div>
@ -204,25 +247,27 @@
<div class="tab-content">
<div class="tab-pane active" id="temp">
{% if enableTemperatureGraph %}
<div class="row" style="padding-left: 20px">
<div id="temperature-graph"></div>
</div>
{% endif %}
<div class="row-fluid" style="margin-bottom: 20px">
<div class="form-horizontal span6">
<h1>Temperature</h1>
<label>Current: <strong data-bind="html: tempString"></strong></label>
<label title="Current extruder temperature">Current: <strong data-bind="html: tempString"></strong></label>
<label>Target: <strong data-bind="html: targetTempString"></strong></label>
<label title="Target extruder temperature">Target: <strong data-bind="html: targetTempString"></strong></label>
<div style="display: none;" data-bind="visible: loginState.isUser">
<label for="temp_newTemp">New Target</label>
<label for="temp_newTemp" title="Sets the new target temperature for the extruder">New Target</label>
<div class="input-append">
<input type="text" id="temp_newTemp" data-bind="attr: {placeholder: targetTemp}, enable: isOperational() && loginState.isUser()" class="tempInput">
<input type="text" class="input-mini text-right" data-bind="value: newTemp, valueUpdate: 'afterkeydown', attr: {placeholder: targetTemp}, enable: isOperational() && loginState.isUser(), event: { keyup: function(d, e) {$root.handleEnter(e, 'temp');} }" class="tempInput">
<span class="add-on">&deg;C</span>
</div>
<div class="btn-group">
<button type="submit" class="btn" id="temp_newTemp_set" data-bind="enable: isOperational() && loginState.isUser()">Set</button>
<button type="submit" class="btn" data-bind="click: setTemp, enable: isOperational() && loginState.isUser() && newTemp()">Set</button>
<button class="btn dropdown-toggle" data-toggle="dropdown" data-bind="enable: isOperational() && loginState.isUser()">
<span class="caret"></span>
</button>
@ -234,27 +279,35 @@
<!-- /ko -->
<li class="divider"></li>
<li>
<a href="#" data-bind="click: function() { $root.setTemp(0); }">Off</a>
<a href="#" data-bind="click: function() { $root.setTempToZero(); }">Off</a>
</li>
</ul>
</div>
</div>
<div style="display: none;" data-bind="visible: loginState.isUser">
<label title="Sets a temperature offset to apply to temperatures set via streamed GCODE, may be positive or negative, will not persist across restarts of OctoPrint">Offset</label>
<div class="input-append">
<input type="number" min="-50" max="50" class="input-mini text-right" data-bind="value: newTempOffset, valueUpdate: 'afterkeydown', attr: {placeholder: tempOffset}, enable: isOperational() && loginState.isUser(), event: { keyup: function(d, e) {$root.handleEnter(e, 'tempOffset');} }" class="tempInput">
<span class="add-on">&deg;C</span>
</div>
<button type="submit" class="btn" data-bind="click: setTempOffset, enable: newTempOffset() && isOperational() && loginState.isUser()">Set</button>
</div>
</div>
<div class="form-horizontal span6">
<h1>Bed Temperature</h1>
<label>Current: <strong data-bind="html: bedTempString"></strong></label>
<label title="Current bed temperature">Current: <strong data-bind="html: bedTempString"></strong></label>
<label>Target: <strong data-bind="html: bedTargetTempString"></strong></label>
<label title="Target bed temperature">Target: <strong data-bind="html: bedTargetTempString"></strong></label>
<div style="display: none;" data-bind="visible: loginState.isUser">
<label for="temp_newBedTemp">New Target</label>
<label for="temp_newBedTemp" title="Sets the new target temperature for the bed">New Target</label>
<div class="input-append">
<input type="text" id="temp_newBedTemp" data-bind="attr: {placeholder: bedTargetTemp}, enable: isOperational() && loginState.isUser()" class="tempInput">
<input type="text" class="input-mini text-right" data-bind="value: newBedTemp, valueUpdate: 'afterkeydown', attr: {placeholder: bedTargetTemp}, enable: isOperational() && loginState.isUser(), event: { keyup: function(d, e) {$root.handleEnter(event, 'bedTemp');} }" class="tempInput">
<span class="add-on">&deg;C</span>
</div>
<div class="btn-group">
<button type="submit" class="btn" id="temp_newBedTemp_set" data-bind="enable: isOperational() && loginState.isUser()">Set</button>
<button type="submit" class="btn" data-bind="click: setBedTemp, enable: isOperational() && loginState.isUser() && newBedTemp()">Set</button>
<button class="btn dropdown-toggle" data-toggle="dropdown" data-bind="enable: isOperational() && loginState.isUser()">
<span class="caret"></span>
</button>
@ -266,18 +319,26 @@
<!-- /ko -->
<li class="divider"></li>
<li>
<a href="#" data-bind="click: function(){ $root.setBedTemp(0); }">Off</a>
<a href="#" data-bind="click: function(){ $root.setBedTempToZero(); }">Off</a>
</li>
</ul>
</div>
</div>
<div style="display: none;" data-bind="visible: loginState.isUser">
<label title="Sets a temperature offset to apply to bed temperatures set via streamed GCODE, may be positive or negative, will not persist across restarts of OctoPrint">Offset</label>
<div class="input-append">
<input type="number" min="-50" max="50" class="input-mini text-right" data-bind="value: newBedTempOffset, valueUpdate: 'afterkeydown', attr: {placeholder: bedTempOffset}, enable: isOperational() && loginState.isUser(), event: { keyup: function(d, e) {$root.handleEnter(e, 'bedTempOffset');} }" class="tempInput">
<span class="add-on">&deg;C</span>
</div>
<button type="submit" class="btn" data-bind="click: setBedTempOffset, enable: newBedTempOffset() && isOperational() && loginState.isUser()">Set</button>
</div>
</div>
</div>
</div>
<div class="tab-pane" id="control">
{% if webcamStream %}
<div id="webcam_container">
<img id="webcam_image" src="{{ webcamStream }}">
<img id="webcam_image" src="{{ webcamStream }}" data-bind="css: { flipH: settings.webcam_flipH(), flipV: settings.webcam_flipV() }">
</div>
{% endif %}
@ -356,6 +417,16 @@
<button class="btn" data-bind="text: name, enable: $root.isOperational() && $root.loginState.isUser(), click: function() { $root.sendCustomCommand($data) }"></button>
</form>
</script>
<script type="text/html" id="customControls_feedbackCommandTemplate">
<form class="form-inline">
<button class="btn" data-bind="text: name, enable: $root.isOperational() && $root.loginState.isUser(), click: function() { $root.sendCustomCommand($data) }"></button> <span data-bind="text: output"></span>
</form>
</script>
<script type="text/html" id="customControls_feedbackTemplate">
<div>
<strong data-bind="text: name"></strong>: <span data-bind="text: output"></span>
</div>
</script>
<script type="text/html" id="customControls_parametricCommandTemplate">
<form class="form-inline">
<!-- ko foreach: input -->
@ -368,6 +439,7 @@
<script type="text/html" id="customControls_emptyTemplate"><div></div></script>
<!-- End of templates for custom controls -->
</div>
{% if enableGCodeVisualizer %}
<div class="tab-pane" id="gcode">
<canvas id="canvas" width="572" height="588"></canvas>
<div id="slider-vertical"></div>
@ -455,15 +527,21 @@
</div>
</div>
{% endif %}
<div class="tab-pane" id="term">
<pre id="terminal-output" class="pre-scrollable"></pre>
<label class="checkbox">
<input type="checkbox" id="terminal-autoscroll" data-bind="checked: autoscrollEnabled"> Autoscroll
</label>
<div data-bind="foreach: filters">
<label class="checkbox">
<input type="checkbox" data-bind="attr: { value: regex }, checked: $parent.activeFilters"> <span data-bind="text: name"></span>
</label>
</div>
<div class="input-append" style="display: none;" data-bind="visible: loginState.isUser">
<input type="text" id="terminal-command" data-bind="enable: isOperational() && loginState.isUser()">
<button class="btn" type="button" id="terminal-send" data-bind="enable: isOperational() && loginState.isUser()">Send</button>
<input type="text" id="terminal-command" data-bind="value: command, event: { keyup: function(d,e) { handleEnter(e); } }, enable: isOperational() && loginState.isUser()">
<button class="btn" type="button" id="terminal-send" data-bind="click: sendCommand, enable: isOperational() && loginState.isUser()">Send</button>
</div>
</div>
{% if enableTimelapse %}
@ -481,13 +559,19 @@
<div id="webcam_timelapse_timedsettings" data-bind="visible: intervalInputEnabled()">
<label for="webcam_timelapse_interval">Interval</label>
<div class="input-append">
<input type="text" class="input-mini" id="webcam_timelapse_interval" data-bind="value: timelapseTimedInterval, enable: isOperational() && !isPrinting() && loginState.isUser()">
<input type="text" class="input-mini" id="webcam_timelapse_interval" data-bind="value: timelapseTimedInterval, valueUpdate: 'afterkeydown', enable: isOperational() && !isPrinting() && loginState.isUser()">
<span class="add-on">sec</span>
</div>
</div>
<div data-bind="visible: loginState.isAdmin">
<label class="checkbox">
<input type="checkbox" data-bind="checked: persist"> Save as default
</label>
</div>
<div>
<button class="btn" data-bind="click: save, enable: isOperational() && !isPrinting() && loginState.isUser()">Save Settings</button>
<button class="btn" data-bind="click: save, enable: saveButtonEnabled">Save config</button>
</div>
</div>
@ -508,7 +592,7 @@
<tr data-bind="attr: {title: name}">
<td class="timelapse_files_name" data-bind="text: name"></td>
<td class="timelapse_files_size" data-bind="text: size"></td>
<td class="timelapse_files_action"><a href="#" class="icon-trash" data-bind="click: function() { if ($root.loginState.isUser()) { $parent.removeFile(); } else { return; } }, css: {disabled: !$root.loginState.isUser()}"></a>&nbsp;|&nbsp;<a href="#" class="icon-download" data-bind="attr: {href: url}"></a></td>
<td class="timelapse_files_action"><a href="#" class="icon-trash" data-bind="click: function() { if ($root.loginState.isUser()) { $parent.removeFile($data.name); } else { return; } }, css: {disabled: !$root.loginState.isUser()}"></a>&nbsp;|&nbsp;<a href="#" class="icon-download" data-bind="attr: {href: url}"></a></td>
</tr>
</tbody>
</table>
@ -529,7 +613,12 @@
</div>
</div>
<div class="footer">
<ul>
{% if gitBranch and gitCommit %}
<ul class="pull-left muted">
<li><small>Branch: {{ gitBranch }}, Commit: {{ gitCommit }}</small></li>
</ul>
{% endif %}
<ul class="pull-right">
<li><a href="http://octoprint.org"><i class="icon-home"></i> Homepage</a></li>
<li><a href="https://github.com/foosel/OctoPrint/"><i class="icon-download"></i> Sourcecode</a></li>
<li><a href="https://github.com/foosel/OctoPrint/wiki"><i class="icon-book"></i> Documentation</a></li>
@ -541,23 +630,46 @@
{% include 'settings.jinja2' %}
{% include 'dialogs.jinja2' %}
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/modernizr.custom.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/underscore.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/knockout.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/bootstrap/bootstrap.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/bootstrap/bootstrap-modalmanager.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/bootstrap/bootstrap-modal.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery/jquery.ui.core.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery/jquery.ui.widget.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery/jquery.ui.mouse.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery/jquery.ui.slider.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery/jquery.pnotify.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery/jquery.flot.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery/jquery.iframe-transport.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery/jquery.fileupload.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/socket.io/socket.io.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/ui.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/modernizr.custom.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/underscore.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/knockout.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/avltree.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/bootstrap/bootstrap.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/bootstrap/bootstrap-modalmanager.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/bootstrap/bootstrap-modal.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.ui.core.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.ui.widget.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.ui.mouse.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.ui.slider.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.pnotify.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.flot.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.iframe-transport.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.fileupload.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/sockjs-0.3.4.min.js') }}"></script>
<!-- Include OctoPrint files -->
<!-- TODO: merge/minimize in the future -->
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/appearance.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/connection.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/control.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/firstrun.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/gcode.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/gcodefiles.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/loginstate.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/navigation.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/printerstate.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/settings.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/temperature.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/terminal.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/timelapse.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/users.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/dataupdater.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/helpers.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/main.js') }}"></script>
<!-- /Include OctoPrint files -->
<script type="text/javascript" src="{{ url_for('static', filename='gcodeviewer/js/ui.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='gcodeviewer/js/gCodeReader.js') }}"></script>

View File

@ -6,17 +6,80 @@
<div class="modal-body">
<div class="tabbable">
<ul class="nav nav-list span4" id="settingsTabs">
<li class="active"><a href="#settings_printerParameters" data-toggle="tab">Printer Parameters</a></li>
<li><a href="#settings_webcam" data-toggle="tab">Webcam</a></li>
<li class="nav-header">Printer</li>
<li class="active"><a href="#settings_serialConnection" data-toggle="tab">Serial Connection</a></li>
<li><a href="#settings_printerParameters" data-toggle="tab">Printer Parameters</a></li>
<li><a href="#settings_temperature" data-toggle="tab">Temperatures</a></li>
<li><a href="#settings_terminalFilters" data-toggle="tab">Terminal filters</a></li>
<li class="nav-header">Features</li>
<li><a href="#settings_features" data-toggle="tab">Features</a></li>
<li><a href="#settings_folder" data-toggle="tab">Folder</a></li>
<li><a href="#settings_temperature" data-toggle="tab">Temperature</a></li>
<li><a href="#settings_webcam" data-toggle="tab">Webcam</a></li>
{% if enableAccessControl %}<li><a href="#settings_users" data-toggle="tab">Access Control</a></li>{% endif %}
<li><a href="#settings_api" data-toggle="tab">Api</a></li>
<li class="nav-header">OctoPrint</li>
<li><a href="#settings_folder" data-toggle="tab">Folders</a></li>
<li><a href="#settings_appearance" data-toggle="tab">Appearance</a></li>
{% if enableAccessControl %}<li><a href="#settings_users" data-toggle="tab">Users</a></li>{% endif %}
</ul>
<div class="tab-content span8">
<div class="tab-pane active" id="settings_printerParameters">
<div class="tab-pane active" id="settings_serialConnection">
<form class="form-horizontal">
<div class="control-group">
<label class="control-label" for="settings-serialPort">Serial Port</label>
<div class="controls">
<select id="settings-serialPort" data-bind="options: serial_portOptions, optionsCaption: 'AUTO', value: serial_port"></select>
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-baudrate">Baudrate</label>
<div class="controls">
<select id="settings-baudrate" data-bind="options: serial_baudrateOptions, optionsCaption: 'AUTO', value: serial_baudrate"></select>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: serial_autoconnect" id="settings-serialAutoconnect"> Auto-connect to printer on server start
</label>
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-movementSpeedE">Communication timeout</label>
<div class="controls">
<div class="input-append">
<input type="number" step="any" min="0" class="input-mini text-right" data-bind="value: serial_timeoutCommunication" id="settings-serialTimeoutCommunication">
<span class="add-on">s</span>
</div>
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-movementSpeedE">Connection timeout</label>
<div class="controls">
<div class="input-append">
<input type="number" step="any" min="0" class="input-mini text-right" data-bind="value: serial_timeoutConnection" id="settings-serialTimeoutConnection">
<span class="add-on">s</span>
</div>
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-movementSpeedE">Autodetection timeout</label>
<div class="controls">
<div class="input-append">
<input type="number" step="any" min="0" class="input-mini text-right" data-bind="value: serial_timeoutDetection" id="settings-serialTimeoutDetection">
<span class="add-on">s</span>
</div>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: serial_log" id="settings-serialLog"> Log communication to serial.log (might negatively impact performance) <span class="label label-important">Warning</span>
</label>
</div>
</div>
</form>
</div>
<div class="tab-pane" id="settings_printerParameters">
<form class="form-horizontal">
<div class="control-group">
<label class="control-label" for="settings-movementSpeedX">Movement Speed X Axis</label>
@ -89,10 +152,29 @@
</label>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: webcam_flipH" id="settings-webcamFlipH"> Flip webcam horizontally
</label>
</div>
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: webcam_flipV" id="settings-webcamFlipV"> Flip webcam vertically
</label>
</div>
</div>
</form>
</div>
<div class="tab-pane" id="settings_features">
<form class="form-horizontal">
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: feature_temperatureGraph" id="settings-featureTemperatureGraph"> Enable Temperature Graph
</label>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
@ -103,7 +185,28 @@
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: feature_waitForStart" id="settings-featureWaitForStart"> Wait for start on connect
<input type="checkbox" data-bind="checked: feature_sdSupport" id="settings-featureSdSupport"> Enable SD support
</label>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: feature_waitForStart" id="settings-featureWaitForStart"> Wait for <code>start</code> on connect
</label>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: feature_alwaysSendChecksum" id="settings-featureAlwaysSendChecksum"> Send a checksum with <strong>every</strong> command <span class="label">Repetier</span>
</label>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: feature_swallowOkAfterResend" id="settings-swallowOkAfterResend"> Swallow the first "ok" after a resend response <span class="label">Repetier</span>
</label>
</div>
</div>
@ -168,6 +271,32 @@
</div>
</form>
</div>
<div class="tab-pane" id="settings_terminalFilters">
<form class="form-horizontal">
<div class="row-fluid">
<div class="span4"><h4>Name</h4></div>
<div class="span6"><h4>RegExp <small><a href="https://developer.mozilla.org/en/docs/Web/JavaScript/Guide/Regular_Expressions">?</a></small></h4></div>
</div>
<div data-bind="foreach: terminalFilters">
<div class="row-fluid" style="margin-bottom: 5px">
<div class="span4">
<input type="text" class="span12" data-bind="value: name">
</div>
<div class="span6">
<input type="text" class="span12" data-bind="value: regex">
</div>
<div class="span2">
<button title="Remove Filter" class="btn btn-danger" data-bind="click: $parent.removeTerminalFilter"><i class="icon-trash"></i></button>
</div>
</div>
</div>
<div class="row-fluid">
<div class="offset10 span2">
<button title="Add Filter" class="btn btn-primary" data-bind="click: addTerminalFilter"><i class="icon-plus"></i></button>
</div>
</div>
</form>
</div>
<div class="tab-pane" id="settings_appearance">
<form class="form-horizontal">
<div class="control-group">
@ -185,6 +314,23 @@
</div>
</form>
</div>
<div class="tab-pane" id="settings_api">
<form class="form-horizontal">
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" id="settings-apiEnabled" data-bind="checked: api_enabled"> Enable
</label>
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-apiKey">Apikey</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: api_key" id="settings-apikey">
</div>
</div>
</form>
</div>
{% if enableAccessControl %}
<div class="tab-pane" id="settings_users">

View File

@ -1,12 +1,9 @@
# coding=utf-8
import logging
__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
from octoprint.settings import settings
import octoprint.util as util
import logging
import os
import threading
import urllib
@ -14,9 +11,13 @@ import time
import subprocess
import fnmatch
import datetime
import sys
import octoprint.util as util
from octoprint.settings import settings
from octoprint.events import eventManager
def getFinishedTimelapses():
files = []
basedir = settings().getBaseFolder("timelapse")
@ -32,10 +33,29 @@ def getFinishedTimelapses():
})
return files
validTimelapseTypes = ["off", "timed", "zchange"]
updateCallbacks = []
def registerCallback(callback):
if not callback in updateCallbacks:
updateCallbacks.append(callback)
def unregisterCallback(callback):
if callback in updateCallbacks:
updateCallbacks.remove(callback)
def notifyCallbacks(timelapse):
for callback in updateCallbacks:
if timelapse is None:
config = None
else:
config = timelapse.configData()
try: callback.sendTimelapseConfig(config)
except: pass
class Timelapse(object):
def __init__(self):
self._logger = logging.getLogger(__name__)
self._imageNumber = None
self._inTimelapse = False
self._gcodeFile = None
@ -47,17 +67,66 @@ class Timelapse(object):
self._renderThread = None
self._captureMutex = threading.Lock()
def onPrintjobStarted(self, gcodeFile):
self.startTimelapse(gcodeFile)
# subscribe events
eventManager().subscribe("PrintStarted", self.onPrintStarted)
eventManager().subscribe("PrintFailed", self.onPrintDone)
eventManager().subscribe("PrintDone", self.onPrintDone)
eventManager().subscribe("PrintResumed", self.onPrintResumed)
for (event, callback) in self.eventSubscriptions():
eventManager().subscribe(event, callback)
def onPrintjobStopped(self):
def unload(self):
if self._inTimelapse:
self.stopTimelapse(doCreateMovie=False)
# unsubscribe events
eventManager().unsubscribe("PrintStarted", self.onPrintStarted)
eventManager().unsubscribe("PrintFailed", self.onPrintDone)
eventManager().unsubscribe("PrintDone", self.onPrintDone)
eventManager().unsubscribe("PrintResumed", self.onPrintResumed)
for (event, callback) in self.eventSubscriptions():
eventManager().unsubscribe(event, callback)
def onPrintStarted(self, event, payload):
"""
Override this to perform additional actions upon start of a print job.
"""
self.startTimelapse(payload)
def onPrintDone(self, event, payload):
"""
Override this to perform additional actions upon the stop of a print job.
"""
self.stopTimelapse()
def onPrintjobProgress(self, oldPos, newPos, percentage):
pass
def onPrintResumed(self, event, payload):
"""
Override this to perform additional actions upon the pausing of a print job.
"""
if not self._inTimelapse:
self.startTimelapse(payload)
def onZChange(self, oldZ, newZ):
pass
def eventSubscriptions(self):
"""
Override this method to subscribe to additional events by returning an array of (event, callback) tuples.
Events that are already subscribed:
* PrintStarted - self.onPrintStarted
* PrintResumed - self.onPrintResumed
* PrintFailed - self.onPrintDone
* PrintDone - self.onPrintDone
"""
return []
def configData(self):
"""
Override this method to return the current timelapse configuration data. The data should have the following
form:
type: "<type of timelapse>",
options: { <additional options> }
"""
return None
def startTimelapse(self, gcodeFile):
self._logger.debug("Starting timelapse for %s" % gcodeFile)
@ -67,11 +136,13 @@ class Timelapse(object):
self._inTimelapse = True
self._gcodeFile = os.path.basename(gcodeFile)
def stopTimelapse(self):
def stopTimelapse(self, doCreateMovie=True):
self._logger.debug("Stopping timelapse")
self._renderThread = threading.Thread(target=self._createMovie)
self._renderThread.daemon = True
self._renderThread.start()
if doCreateMovie:
self._renderThread = threading.Thread(target=self._createMovie)
self._renderThread.daemon = True
self._renderThread.start()
self._imageNumber = None
self._inTimelapse = False
@ -85,20 +156,21 @@ class Timelapse(object):
filename = os.path.join(self._captureDir, "tmp_%05d.jpg" % (self._imageNumber))
self._imageNumber += 1
self._logger.debug("Capturing image to %s" % filename)
captureThread = threading.Thread(target=self._captureWorker, kwargs={"filename": filename})
captureThread.daemon = True
captureThread.start()
def _captureWorker(self, filename):
eventManager().fire("CaptureStart", filename);
urllib.urlretrieve(self._snapshotUrl, filename)
self._logger.debug("Image %s captured from %s" % (filename, self._snapshotUrl))
eventManager().fire("CaptureDone", filename);
def _createMovie(self):
ffmpeg = settings().get(["webcam", "ffmpeg"])
bitrate = settings().get(["webcam", "bitrate"])
if ffmpeg is None or bitrate is None:
self._logger.warn("Cannot create movie, path to ffmpeg is unset")
self._logger.warn("Cannot create movie, path to ffmpeg or desired bitrate is unset")
return
input = os.path.join(self._captureDir, "tmp_%05d.jpg")
@ -109,19 +181,46 @@ class Timelapse(object):
ffmpeg, '-i', input, '-vcodec', 'mpeg2video', '-pix_fmt', 'yuv420p', '-r', '25', '-y', '-b:v', bitrate,
'-f', 'vob']
filters = []
# flip video if configured
if settings().getBoolean(["webcam", "flipH"]):
filters.append('hflip')
if settings().getBoolean(["webcam", "flipV"]):
filters.append('vflip')
# add watermark if configured
watermarkFilter = None
if settings().getBoolean(["webcam", "watermark"]):
watermark = os.path.join(os.path.dirname(__file__), "static", "img", "watermark.png")
if sys.platform == "win32":
# Because ffmpeg hiccups on windows' drive letters and backslashes we have to give the watermark
# path a special treatment. Yeah, I couldn't believe it either...
watermark = watermark.replace("\\", "/").replace(":", "\\\\:")
command.extend(['-vf', 'movie=%s [wm]; [in][wm] overlay=10:main_h-overlay_h-10 [out]' % watermark])
watermarkFilter = "movie=%s [wm]; [%%(inputName)s][wm] overlay=10:main_h-overlay_h-10" % watermark
filterstring = None
if len(filters) > 0:
if watermarkFilter is not None:
filterstring = "[in] %s [postprocessed]; %s [out]" % (",".join(filters), watermarkFilter % {"inputName": "postprocessed"})
else:
filterstring = "[in] %s [out]" % ",".join(filters)
elif watermarkFilter is not None:
filterstring = watermarkFilter % {"inputName": "in"} + " [out]"
if filterstring is not None:
self._logger.debug("Applying videofilter chain: %s" % filterstring)
command.extend(["-vf", filterstring])
# finalize command with output file
command.append(output)
subprocess.call(command)
self._logger.debug("Rendering movie to %s" % output)
command.append(output)
try:
subprocess.check_call(command)
eventManager().fire("MovieDone", output)
except subprocess.CalledProcessError as (e):
self._logger.warn("Could not render movie, got return code %r" % e.returncode)
def cleanCaptureDir(self):
if not os.path.isdir(self._captureDir):
@ -138,35 +237,53 @@ class ZTimelapse(Timelapse):
Timelapse.__init__(self)
self._logger.debug("ZTimelapse initialized")
def onZChange(self, oldZ, newZ):
self._logger.debug("Z change detected, capturing image")
def eventSubscriptions(self):
return [
("ZChange", self._onZChange)
]
def configData(self):
return {
"type": "zchange"
}
def _onZChange(self, event, payload):
self.captureImage()
class TimedTimelapse(Timelapse):
def __init__(self, interval=1):
Timelapse.__init__(self)
self._interval = interval
if self._interval < 1:
self._interval = 1 # force minimum interval of 1s
self._timerThread = None
self._logger.debug("TimedTimelapse initialized")
def interval(self):
return self._interval
def onPrintjobStarted(self, filename):
Timelapse.onPrintjobStarted(self, filename)
def configData(self):
return {
"type": "timed",
"options": {
"interval": self._interval
}
}
def onPrintStarted(self, event, payload):
Timelapse.onPrintStarted(self, event, payload)
if self._timerThread is not None:
return
self._timerThread = threading.Thread(target=self.timerWorker)
self._timerThread = threading.Thread(target=self._timerWorker)
self._timerThread.daemon = True
self._timerThread.start()
def timerWorker(self):
def onPrintDone(self, event, payload):
Timelapse.onPrintDone(self, event, payload)
self._timerThread = None
def _timerWorker(self):
self._logger.debug("Starting timer for interval based timelapse")
while self._inTimelapse:
self.captureImage()

View File

@ -45,6 +45,8 @@ class UserManager(object):
def getAllUsers(self):
return []
def hasBeenCustomized(self):
return False
class HackerspaceUserManager(UserManager):
"""A user manager for the Warsaw Hackerspace, uses interal apis."""
@ -77,17 +79,19 @@ class FilebasedUserManager(UserManager):
self._users = {}
self._dirty = False
self._customized = None
self._load()
def _load(self):
if os.path.exists(self._userfile) and os.path.isfile(self._userfile):
self._customized = True
with open(self._userfile, "r") as f:
data = yaml.safe_load(f)
for name in data.keys():
attributes = data[name]
self._users[name] = User(name, attributes["password"], attributes["active"], attributes["roles"])
else:
self._users["admin"] = User("admin", "7557160613d5258f883014a7c3c0428de53040fc152b1791f1cc04a62b428c0c2a9c46ed330cdce9689353ab7a5352ba2b2ceb459b96e9c8ed7d0cb0b2c0c076", True, ["user", "admin"])
self._customized = False
def _save(self, force=False):
if not self._dirty and not force:
@ -189,6 +193,9 @@ class FilebasedUserManager(UserManager):
def getAllUsers(self):
return map(lambda x: x.asDict(), self._users.values())
def hasBeenCustomized(self):
return self._customized
##~~ Exceptions
class UserAlreadyExists(Exception):

View File

@ -2,6 +2,5 @@ The code in this sub package mostly originates from the Cura project (https://gi
slightly reorganized and adapted. The mapping to the original Cura source is the following:
* avr_isp.* => Cura.avr_isp.*
* comm => Cura.util.machineCom
* comm => Cura.util.machineCom (highly modified now)
* gcodeInterpreter => Cura.util.gcodeInterpreter
* util3d => Cura.util.util3d

View File

@ -2,9 +2,17 @@
__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
import os
import traceback
import sys
import time
import re
from octoprint.settings import settings
def getFormattedSize(num):
"""
Taken from http://stackoverflow.com/questions/1094841/reusable-library-to-get-human-readable-version-of-file-size
Taken from http://stackoverflow.com/a/1094933/2028598
"""
for x in ["bytes","KB","MB","GB"]:
if num < 1024.0:
@ -12,9 +20,11 @@ def getFormattedSize(num):
num /= 1024.0
return "%3.1f%s" % (num, "TB")
def isAllowedFile(filename, extensions):
return "." in filename and filename.rsplit(".", 1)[1] in extensions
def getFormattedTimeDelta(d):
if d is None:
return None
@ -23,19 +33,108 @@ def getFormattedTimeDelta(d):
seconds = d.seconds % 60
return "%02d:%02d:%02d" % (hours, minutes, seconds)
def getFormattedDateTime(d):
if d is None:
return None
return d.strftime("%Y-%m-%d %H:%M")
def getClass(name):
"""
Taken from http://stackoverflow.com/a/452981/2028598
Taken from http://stackoverflow.com/a/452981/2028598
"""
parts = name.split(".")
module = ".".join(parts[:-1])
m = __import__(module)
for comp in parts[1:]:
m = getattr(m, comp)
return m
return m
def isDevVersion():
gitPath = os.path.abspath(os.path.join(os.path.split(os.path.abspath(__file__))[0], "../../.git"))
return os.path.exists(gitPath)
def getExceptionString():
locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
return "%s: '%s' @ %s:%s:%d" % (str(sys.exc_info()[0].__name__), str(sys.exc_info()[1]), os.path.basename(locationInfo[0]), locationInfo[2], locationInfo[1])
def getGitInfo():
gitPath = os.path.abspath(os.path.join(os.path.split(os.path.abspath(__file__))[0], "../../.git"))
if not os.path.exists(gitPath):
return (None, None)
headref = None
with open(os.path.join(gitPath, "HEAD"), "r") as f:
headref = f.readline().strip()
if headref is None:
return (None, None)
headref = headref[len("ref: "):]
branch = headref[headref.rfind("/") + 1:]
with open(os.path.join(gitPath, headref)) as f:
head = f.readline().strip()
return (branch, head)
def getNewTimeout(type):
now = time.time()
if type not in ["connection", "detection", "communication"]:
return now # timeout immediately for unknown timeout type
return now + settings().getFloat(["serial", "timeout", type])
def getFreeBytes(path):
"""
Taken from http://stackoverflow.com/a/2372171/2028598
"""
if sys.platform == "win32":
import ctypes
freeBytes = ctypes.c_ulonglong(0)
ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(path), None, None, ctypes.pointer(freeBytes))
return freeBytes.value
else:
st = os.statvfs(path)
return st.f_bavail * st.f_frsize
def getRemoteAddress(request):
forwardedFor = request.headers.get("X-Forwarded-For", None)
if forwardedFor is not None:
return forwardedFor.split(",")[0]
return request.remote_addr
def getDosFilename(input, existingFilenames, extension=None):
if input is None:
return None
if extension is None:
extension = "gco"
filename, ext = input.rsplit(".", 1)
return findCollisionfreeName(filename, extension, existingFilenames)
def findCollisionfreeName(input, extension, existingFilenames):
filename = re.sub(r"\s+", "_", input.lower().translate(None, ".\"/\\[]:;=,"))
counter = 1
power = 1
while counter < (10 * power):
result = filename[:(6 - power + 1)] + "~" + str(counter) + "." + extension
if result not in existingFilenames:
return result
counter += 1
if counter == 10 * power:
power += 1
raise ValueError("Can't create a collision free filename")

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,13 @@
from __future__ import absolute_import
__copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
import sys
import math
import re
import os
import base64
import zlib
import logging
from octoprint.util import util3d
from octoprint.settings import settings
preferences = {
"extruder_offset_x1": -22.0,
@ -25,282 +27,208 @@ def getPreference(key, default=None):
class AnalysisAborted(Exception):
pass
class gcodePath(object):
def __init__(self, newType, pathType, layerThickness, startPoint):
self.type = newType
self.pathType = pathType
self.layerThickness = layerThickness
self.list = [startPoint]
class gcode(object):
def __init__(self):
self.regMatch = {}
self.layerList = []
self._logger = logging.getLogger(__name__)
self.layerList = None
self.extrusionAmount = 0
self.totalMoveTimeMinute = 0
self.filename = None
self.progressCallback = None
self._abort = False
self._filamentDiameter = 0
def load(self, filename):
if os.path.isfile(filename):
self.filename = filename
self._fileSize = os.stat(filename).st_size
gcodeFile = open(filename, 'r')
self._load(gcodeFile)
gcodeFile.close()
def loadList(self, l):
self._load(l)
with open(filename, "r") as f:
self._load(f)
def abort(self):
self._abort = True
def calculateVolumeCm3(self):
radius = self._filamentDiameter / 2
return (self.extrusionAmount * (math.pi * radius * radius)) / 1000
def _load(self, gcodeFile):
filePos = 0
pos = util3d.Vector3()
posOffset = util3d.Vector3()
pos = [0.0, 0.0, 0.0]
posOffset = [0.0, 0.0, 0.0]
currentE = 0.0
totalExtrusion = 0.0
maxExtrusion = 0.0
currentExtruder = 0
extrudeAmountMultiply = 1.0
totalMoveTimeMinute = 0.0
absoluteE = True
scale = 1.0
posAbs = True
posAbsExtruder = True;
feedRate = 3600
layerThickness = 0.1
pathType = 'CUSTOM';
startCodeDone = False
currentLayer = []
unknownGcodes={}
unknownMcodes={}
currentPath = gcodePath('move', pathType, layerThickness, pos.copy())
currentPath.list[0].e = totalExtrusion
currentPath.list[0].extrudeAmountMultiply = extrudeAmountMultiply
currentLayer.append(currentPath)
feedRateXY = settings().getFloat(["printerParameters", "movementSpeed", "x"])
for line in gcodeFile:
if self._abort:
raise AnalysisAborted()
if type(line) is tuple:
line = line[0]
if self.progressCallback != None:
if isinstance(gcodeFile, (file)):
self.progressCallback(float(filePos) / float(self._fileSize))
elif isinstance(gcodeFile, (list)):
self.progressCallback(float(filePos) / float(len(gcodeFile)))
filePos += 1
filePos += 1
try:
if self.progressCallback is not None and (filePos % 1000 == 0):
if isinstance(gcodeFile, (file)):
self.progressCallback(float(gcodeFile.tell()) / float(self._fileSize))
elif isinstance(gcodeFile, (list)):
self.progressCallback(float(filePos) / float(len(gcodeFile)))
except:
pass
#Parse Cura_SF comments
if line.startswith(';TYPE:'):
pathType = line[6:].strip()
if pathType != "CUSTOM":
startCodeDone = True
if ';' in line:
#Slic3r GCode comment parser
comment = line[line.find(';')+1:].strip()
if comment == 'fill':
pathType = 'FILL'
elif comment == 'perimeter':
pathType = 'WALL-INNER'
elif comment == 'skirt':
pathType = 'SKIRT'
if comment.startswith('LAYER:'):
self.layerList.append(currentLayer)
currentLayer = []
if pathType != "CUSTOM":
startCodeDone = True
if comment.startswith("filament_diameter"):
self._filamentDiameter = float(comment.split("=", 1)[1].strip())
elif comment.startswith("CURA_PROFILE_STRING"):
curaOptions = self._parseCuraProfileString(comment)
if "filament_diameter" in curaOptions:
try:
self._filamentDiameter = float(curaOptions["filament_diameter"])
except:
self._filamentDiameter = 0.0
line = line[0:line.find(';')]
T = self.getCodeInt(line, 'T')
T = getCodeInt(line, 'T')
if T is not None:
if currentExtruder > 0:
posOffset.x -= getPreference('extruder_offset_x%d' % (currentExtruder), 0.0)
posOffset.y -= getPreference('extruder_offset_y%d' % (currentExtruder), 0.0)
posOffset[0] -= getPreference('extruder_offset_x%d' % (currentExtruder), 0.0)
posOffset[1] -= getPreference('extruder_offset_y%d' % (currentExtruder), 0.0)
currentExtruder = T
if currentExtruder > 0:
posOffset.x += getPreference('extruder_offset_x%d' % (currentExtruder), 0.0)
posOffset.y += getPreference('extruder_offset_y%d' % (currentExtruder), 0.0)
posOffset[0] += getPreference('extruder_offset_x%d' % (currentExtruder), 0.0)
posOffset[1] += getPreference('extruder_offset_y%d' % (currentExtruder), 0.0)
G = self.getCodeInt(line, 'G')
G = getCodeInt(line, 'G')
if G is not None:
if G == 0 or G == 1: #Move
x = self.getCodeFloat(line, 'X')
y = self.getCodeFloat(line, 'Y')
z = self.getCodeFloat(line, 'Z')
e = self.getCodeFloat(line, 'E')
f = self.getCodeFloat(line, 'F')
oldPos = pos.copy()
if x is not None:
if posAbs:
pos.x = x * scale + posOffset.x
else:
pos.x += x * scale
if y is not None:
if posAbs:
pos.y = y * scale + posOffset.y
else:
pos.y += y * scale
if z is not None:
if posAbs:
pos.z = z * scale + posOffset.z
else:
pos.z += z * scale
x = getCodeFloat(line, 'X')
y = getCodeFloat(line, 'Y')
z = getCodeFloat(line, 'Z')
e = getCodeFloat(line, 'E')
f = getCodeFloat(line, 'F')
oldPos = pos
pos = pos[:]
if posAbs:
if x is not None:
pos[0] = x * scale + posOffset[0]
if y is not None:
pos[1] = y * scale + posOffset[1]
if z is not None:
pos[2] = z * scale + posOffset[2]
else:
if x is not None:
pos[0] += x * scale
if y is not None:
pos[1] += y * scale
if z is not None:
pos[2] += z * scale
if f is not None:
feedRate = f
feedRateXY = f
if x is not None or y is not None or z is not None:
totalMoveTimeMinute += (oldPos - pos).vsize() / feedRate
diffX = oldPos[0] - pos[0]
diffY = oldPos[1] - pos[1]
totalMoveTimeMinute += math.sqrt(diffX * diffX + diffY * diffY) / feedRateXY
moveType = 'move'
if e is not None:
if posAbsExtruder:
if e > currentE:
moveType = 'extrude'
if e < currentE:
moveType = 'retract'
totalExtrusion += e - currentE
currentE = e
else:
if e > 0:
moveType = 'extrude'
if e < 0:
moveType = 'retract'
totalExtrusion += e
currentE += e
if absoluteE:
e -= currentE
if e > 0.0:
moveType = 'extrude'
if e < 0.0:
moveType = 'retract'
totalExtrusion += e
currentE += e
if totalExtrusion > maxExtrusion:
maxExtrusion = totalExtrusion
if moveType == 'move' and oldPos.z != pos.z:
if oldPos.z > pos.z and abs(oldPos.z - pos.z) > 5.0 and pos.z < 1.0:
oldPos.z = 0.0
layerThickness = abs(oldPos.z - pos.z)
if currentPath.type != moveType or currentPath.pathType != pathType:
currentPath = gcodePath(moveType, pathType, layerThickness, currentPath.list[-1])
currentLayer.append(currentPath)
newPos = pos.copy()
newPos.e = totalExtrusion
newPos.extrudeAmountMultiply = extrudeAmountMultiply
currentPath.list.append(newPos)
else:
e = 0.0
if moveType == 'move' and oldPos[2] != pos[2]:
if oldPos[2] > pos[2] and abs(oldPos[2] - pos[2]) > 5.0 and pos[2] < 1.0:
oldPos[2] = 0.0
elif G == 4: #Delay
S = self.getCodeFloat(line, 'S')
S = getCodeFloat(line, 'S')
if S is not None:
totalMoveTimeMinute += S / 60
P = self.getCodeFloat(line, 'P')
totalMoveTimeMinute += S / 60.0
P = getCodeFloat(line, 'P')
if P is not None:
totalMoveTimeMinute += P / 60 / 1000
totalMoveTimeMinute += P / 60.0 / 1000.0
elif G == 20: #Units are inches
scale = 25.4
elif G == 21: #Units are mm
scale = 1.0
elif G == 28: #Home
x = self.getCodeFloat(line, 'X')
y = self.getCodeFloat(line, 'Y')
z = self.getCodeFloat(line, 'Z')
x = getCodeFloat(line, 'X')
y = getCodeFloat(line, 'Y')
z = getCodeFloat(line, 'Z')
center = [0.0,0.0,0.0]
if x is None and y is None and z is None:
pos = util3d.Vector3()
pos = center
else:
pos = pos[:]
if x is not None:
pos.x = 0.0
pos[0] = center[0]
if y is not None:
pos.y = 0.0
pos[1] = center[1]
if z is not None:
pos.z = 0.0
pos[2] = center[2]
elif G == 90: #Absolute position
posAbs = True
posAbsExtruder = True
elif G == 91: #Relative position
posAbs = False
posAbsExtruder = False
elif G == 92:
x = self.getCodeFloat(line, 'X')
y = self.getCodeFloat(line, 'Y')
z = self.getCodeFloat(line, 'Z')
e = self.getCodeFloat(line, 'E')
x = getCodeFloat(line, 'X')
y = getCodeFloat(line, 'Y')
z = getCodeFloat(line, 'Z')
e = getCodeFloat(line, 'E')
if e is not None:
currentE = e
if x is not None:
posOffset.x = pos.x - x
posOffset[0] = pos[0] - x
if y is not None:
posOffset.y = pos.y - y
posOffset[1] = pos[1] - y
if z is not None:
posOffset.z = pos.z - z
else:
if G not in unknownGcodes:
print "Unknown G code:" + str(G)
unknownGcodes[G] = True
posOffset[2] = pos[2] - z
else:
M = self.getCodeInt(line, 'M')
M = getCodeInt(line, 'M')
if M is not None:
if M == 1: #Message with possible wait (ignored)
pass
elif M == 80: #Enable power supply
pass
elif M == 81: #Suicide/disable power supply
pass
elif M == 82: # Use absolute extruder positions
posAbsExtruder = True
elif M == 83: # Use relative extruder positions
posAbsExtruder = False
elif M == 84: #Disable step drivers
pass
elif M == 92: #Set steps per unit
pass
elif M == 101: #Enable extruder
pass
elif M == 103: #Disable extruder
pass
elif M == 104: #Set temperature, no wait
pass
elif M == 105: #Get temperature
pass
elif M == 106: #Enable fan
pass
elif M == 107: #Disable fan
pass
elif M == 108: #Extruder RPM (these should not be in the final GCode, but they are)
pass
elif M == 109: #Set temperature, wait
pass
elif M == 110: #Reset N counter
pass
elif M == 113: #Extruder PWM (these should not be in the final GCode, but they are)
pass
elif M == 140: #Set bed temperature
pass
elif M == 190: #Set bed temperature & wait
pass
elif M == 221: #Extrude amount multiplier
s = self.getCodeFloat(line, 'S')
if s != None:
extrudeAmountMultiply = s / 100.0
else:
if M not in unknownMcodes:
print "Unknown M code:" + str(M)
unknownMcodes[M] = True
self.layerList.append(currentLayer)
if M == 82: #Absolute E
absoluteE = True
elif M == 83: #Relative E
absoluteE = False
if self.progressCallback is not None:
self.progressCallback(100.0)
self.extrusionAmount = maxExtrusion
self.totalMoveTimeMinute = totalMoveTimeMinute
def getCodeInt(self, line, code):
if code not in self.regMatch:
self.regMatch[code] = re.compile(code + '([^\s]+)')
m = self.regMatch[code].search(line)
if m == None:
return None
try:
return int(m.group(1))
except:
return None
def _parseCuraProfileString(self, comment):
return {key: value for (key, value) in map(lambda x: x.split("=", 1), zlib.decompress(base64.b64decode(comment[len("CURA_PROFILE_STRING:"):])).split("\b"))}
def getCodeFloat(self, line, code):
if code not in self.regMatch:
self.regMatch[code] = re.compile(code + '([^\s]+)')
m = self.regMatch[code].search(line)
if m == None:
return None
try:
return float(m.group(1))
except:
return None
if __name__ == '__main__':
for filename in sys.argv[1:]:
gcode().load(filename)
def getCodeInt(line, code):
n = line.find(code) + 1
if n < 1:
return None
m = line.find(' ', n)
try:
if m < 0:
return int(line[n:])
return int(line[n:m])
except:
return None
def getCodeFloat(line, code):
n = line.find(code) + 1
if n < 1:
return None
m = line.find(' ', n)
try:
if m < 0:
return float(line[n:])
return float(line[n:m])
except:
return None

View File

@ -1,317 +0,0 @@
from __future__ import absolute_import
import math
import numpy
class Vector3(object):
def __init__(self, x=0.0, y=0.0, z=0.0):
self.x = x
self.y = y
self.z = z
def __copy__(self):
return Vector3(self.x, self.y, self.z)
def copy(self):
return Vector3(self.x, self.y, self.z)
def __repr__(self):
return 'V[%s, %s, %s]' % ( self.x, self.y, self.z )
def __add__(self, v):
return Vector3( self.x + v.x, self.y + v.y, self.z + v.z )
def __sub__(self, v):
return Vector3( self.x - v.x, self.y - v.y, self.z - v.z )
def __mul__(self, v):
return Vector3( self.x * v, self.y * v, self.z * v )
def __div__(self, v):
return Vector3( self.x / v, self.y / v, self.z / v )
__truediv__ = __div__
def __neg__(self):
return Vector3( - self.x, - self.y, - self.z )
def __iadd__(self, v):
self.x += v.x
self.y += v.y
self.z += v.z
return self
def __isub__(self, v):
self.x += v.x
self.y += v.y
self.z += v.z
return self
def __imul__(self, v):
self.x *= v
self.y *= v
self.z *= v
return self
def __idiv__(self, v):
self.x /= v
self.y /= v
self.z /= v
return self
def almostEqual(self, v):
return (abs(self.x - v.x) + abs(self.y - v.y) + abs(self.z - v.z)) < 0.00001
def cross(self, v):
return Vector3(self.y * v.z - self.z * v.y, -self.x * v.z + self.z * v.x, self.x * v.y - self.y * v.x)
def vsize(self):
return math.sqrt( self.x * self.x + self.y * self.y + self.z * self.z )
def normalize(self):
f = self.vsize()
if f != 0.0:
self.x /= f
self.y /= f
self.z /= f
def min(self, v):
return Vector3(min(self.x, v.x), min(self.y, v.y), min(self.z, v.z))
def max(self, v):
return Vector3(max(self.x, v.x), max(self.y, v.y), max(self.z, v.z))
class AABB(object):
def __init__(self, vMin, vMax):
self.vMin = vMin
self.vMax = vMax
self.perimeter = numpy.sum(self.vMax - self.vMin)
def combine(self, aabb):
return AABB(numpy.minimum(self.vMin, aabb.vMin), numpy.maximum(self.vMax, aabb.vMax))
def overlap(self, aabb):
if aabb.vMin[0] - self.vMax[0] > 0.0 or aabb.vMin[1] - self.vMax[1] > 0.0 or aabb.vMin[2] - self.vMax[2] > 0.0:
return False
if self.vMin[0] - aabb.vMax[0] > 0.0 or self.vMin[1] - aabb.vMax[1] > 0.0 or self.vMin[2] - aabb.vMax[2] > 0.0:
return False
return True
def __repr__(self):
return "AABB:%s - %s" % (str(self.vMin), str(self.vMax))
class _AABBNode(object):
def __init__(self, aabb):
self.child1 = None
self.child2 = None
self.parent = None
self.height = 0
self.aabb = aabb
def isLeaf(self):
return self.child1 == None
class AABBTree(object):
def __init__(self):
self.root = None
def insert(self, aabb):
newNode = _AABBNode(aabb)
if self.root == None:
self.root = newNode
return
node = self.root
while not node.isLeaf():
child1 = node.child1
child2 = node.child2
area = node.aabb.perimeter
combinedAABB = node.aabb.combine(aabb)
combinedArea = combinedAABB.perimeter
cost = 2.0 * combinedArea
inheritanceCost = 2.0 * (combinedArea - area)
if child1.isLeaf():
cost1 = aabb.combine(child1.aabb).perimeter + inheritanceCost
else:
oldArea = child1.aabb.perimeter
newArea = aabb.combine(child1.aabb).perimeter
cost1 = (newArea - oldArea) + inheritanceCost
if child2.isLeaf():
cost2 = aabb.combine(child1.aabb).perimeter + inheritanceCost
else:
oldArea = child2.aabb.perimeter
newArea = aabb.combine(child2.aabb).perimeter
cost2 = (newArea - oldArea) + inheritanceCost
if cost < cost1 and cost < cost2:
break
if cost1 < cost2:
node = child1
else:
node = child2
sibling = node
# Create a new parent.
oldParent = sibling.parent
newParent = _AABBNode(aabb.combine(sibling.aabb))
newParent.parent = oldParent
newParent.height = sibling.height + 1
if oldParent != None:
# The sibling was not the root.
if oldParent.child1 == sibling:
oldParent.child1 = newParent
else:
oldParent.child2 = newParent
newParent.child1 = sibling
newParent.child2 = newNode
sibling.parent = newParent
newNode.parent = newParent
else:
# The sibling was the root.
newParent.child1 = sibling
newParent.child2 = newNode
sibling.parent = newParent
newNode.parent = newParent
self.root = newParent
# Walk back up the tree fixing heights and AABBs
node = newNode.parent
while node != None:
node = self._balance(node)
child1 = node.child1
child2 = node.child2
node.height = 1 + max(child1.height, child2.height)
node.aabb = child1.aabb.combine(child2.aabb)
node = node.parent
def _balance(self, A):
if A.isLeaf() or A.height < 2:
return A
B = A.child1
C = A.child2
balance = C.height - B.height
# Rotate C up
if balance > 1:
F = C.child1;
G = C.child2;
# Swap A and C
C.child1 = A;
C.parent = A.parent;
A.parent = C;
# A's old parent should point to C
if C.parent != None:
if C.parent.child1 == A:
C.parent.child1 = C
else:
C.parent.child2 = C
else:
self.root = C
# Rotate
if F.height > G.height:
C.child2 = F
A.child2 = G
G.parent = A
A.aabb = B.aabb.combine(G.aabb)
C.aabb = A.aabb.combine(F.aabb)
A.height = 1 + max(B.height, G.height)
C.height = 1 + max(A.height, F.height)
else:
C.child2 = G
A.child2 = F
F.parent = A
A.aabb = B.aabb.combine(F.aabb)
C.aabb = A.aabb.combine(G.aabb)
A.height = 1 + max(B.height, F.height)
C.height = 1 + max(A.height, G.height)
return C;
# Rotate B up
if balance < -1:
D = B.child1
E = B.child2
# Swap A and B
B.child1 = A
B.parent = A.parent
A.parent = B
# A's old parent should point to B
if B.parent != None:
if B.parent.child1 == A:
B.parent.child1 = B
else:
B.parent.child2 = B
else:
self.root = B
# Rotate
if D.height > E.height:
B.child2 = D
A.child1 = E
E.parent = A
A.aabb = C.aabb.combine(E.aabb)
B.aabb = A.aabb.combine(D.aabb)
A.height = 1 + max(C.height, E.height)
B.height = 1 + max(A.height, D.height)
else:
B.child2 = E
A.child1 = D
D.parent = A
A.aabb = C.aabb.combine(D.aabb)
B.aabb = A.aabb.combine(E.aabb)
A.height = 1 + max(C.height, D.height)
B.height = 1 + max(A.height, E.height)
return B
return A
def query(self, aabb):
resultList = []
if self.root != None:
self._query(self.root, aabb, resultList)
return resultList
def _query(self, node, aabb, resultList):
if not aabb.overlap(node.aabb):
return
if node.isLeaf():
resultList.append(node.aabb)
else:
self._query(node.child1, aabb, resultList)
self._query(node.child2, aabb, resultList)
def __repr__(self):
s = "AABBTree:\n"
s += str(self.root.aabb)
return s
if __name__ == '__main__':
tree = AABBTree()
tree.insert(AABB(Vector3(0,0,0), Vector3(0,0,0)))
tree.insert(AABB(Vector3(1,1,1), Vector3(1,1,1)))
tree.insert(AABB(Vector3(0.5,0.5,0.5), Vector3(0.5,0.5,0.5)))
print(tree)
print(tree.query(AABB(Vector3(0,0,0), Vector3(0,0,0))))

286
octoprint/util/virtual.py Normal file
View File

@ -0,0 +1,286 @@
from __future__ import absolute_import
# coding=utf-8
__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
import time
import os
import re
import threading
import math
from octoprint.settings import settings
class VirtualPrinter():
def __init__(self):
self.readList = ['start\n', 'Marlin: Virtual Marlin!\n', '\x80\n', 'SD init fail\n'] # no sd card as default startup scenario
self.temp = 0.0
self.targetTemp = 0.0
self.lastTempAt = time.time()
self.bedTemp = 1.0
self.bedTargetTemp = 1.0
self._virtualSd = settings().getBaseFolder("virtualSd")
self._sdCardReady = False
self._sdPrinter = None
self._sdPrintingSemaphore = threading.Event()
self._selectedSdFile = None
self._selectedSdFileSize = None
self._selectedSdFilePos = None
self._writingToSd = False
self._newSdFilePos = None
self.currentLine = 0
self.lastN = 0
waitThread = threading.Thread(target=self._sendWaitAfterTimeout)
waitThread.start()
def write(self, data):
if self.readList is None:
return
data = data.strip()
# strip checksum
if "*" in data:
data = data[:data.rfind("*")]
self.currentLine += 1
elif settings().getBoolean(["devel", "virtualPrinter", "forceChecksum"]):
self.readList.append("Error: Missing checksum")
return
# track N = N + 1
if data.startswith("N") and "M110" in data:
linenumber = int(re.search("N([0-9]+)", data).group(1))
self.lastN = linenumber
self.currentLine = linenumber
return
elif data.startswith("N"):
linenumber = int(re.search("N([0-9]+)", data).group(1))
expected = self.lastN + 1
if linenumber != expected:
self.readList.append("Error: expected line %d got %d" % (expected, linenumber))
self.readList.append("Resend:%d" % expected)
if settings().getBoolean(["devel", "virtualPrinter", "okAfterResend"]):
self.readList.append("ok")
return
elif self.currentLine == 100:
# simulate a resend at line 100 of the last 5 lines
self.lastN = 94
self.readList.append("Error: Line Number is not Last Line Number\n")
self.readList.append("rs %d\n" % (self.currentLine - 5))
if settings().getBoolean(["devel", "virtualPrinter", "okAfterResend"]):
self.readList.append("ok")
return
else:
self.lastN = linenumber
data = data.split(None, 1)[1].strip()
data += "\n"
# shortcut for writing to SD
if self._writingToSd and not self._selectedSdFile is None and not "M29" in data:
with open(self._selectedSdFile, "a") as f:
f.write(data)
self._sendOk()
return
#print "Send: %s" % (data.rstrip())
if 'M104' in data or 'M109' in data:
try:
self.targetTemp = float(re.search('S([0-9]+)', data).group(1))
except:
pass
if 'M140' in data or 'M190' in data:
try:
self.bedTargetTemp = float(re.search('S([0-9]+)', data).group(1))
except:
pass
if 'M105' in data:
# send simulated temperature data
self.readList.append("ok T:%.2f /%.2f B:%.2f /%.2f @:64\n" % (self.temp, self.targetTemp, self.bedTemp, self.bedTargetTemp))
elif 'M20' in data:
if self._sdCardReady:
self._listSd()
elif 'M21' in data:
self._sdCardReady = True
self.readList.append("SD card ok")
elif 'M22' in data:
self._sdCardReady = False
elif 'M23' in data:
if self._sdCardReady:
filename = data.split(None, 1)[1].strip()
self._selectSdFile(filename)
elif 'M24' in data:
if self._sdCardReady:
self._startSdPrint()
elif 'M25' in data:
if self._sdCardReady:
self._pauseSdPrint()
elif 'M26' in data:
if self._sdCardReady:
pos = int(re.search("S([0-9]+)", data).group(1))
self._setSdPos(pos)
elif 'M27' in data:
if self._sdCardReady:
self._reportSdStatus()
elif 'M28' in data:
if self._sdCardReady:
filename = data.split(None, 1)[1].strip()
self._writeSdFile(filename)
elif 'M29' in data:
if self._sdCardReady:
self._finishSdFile()
elif 'M30' in data:
if self._sdCardReady:
filename = data.split(None, 1)[1].strip()
self._deleteSdFile(filename)
elif "M114" in data:
# send dummy position report
self.readList.append("ok C: X:10.00 Y:3.20 Z:5.20 E:1.24")
elif "M117" in data:
# we'll just use this to echo a message, to allow playing around with pause triggers
self.readList.append("ok %s" % re.search("M117\s+(.*)", data).group(1))
elif "M999" in data:
# mirror Marlin behaviour
self.readList.append("Resend: 1")
elif len(data.strip()) > 0:
self._sendOk()
def _listSd(self):
self.readList.append("Begin file list")
for osFile in os.listdir(self._virtualSd):
self.readList.append(osFile.upper())
self.readList.append("End file list")
self._sendOk()
def _selectSdFile(self, filename):
file = os.path.join(self._virtualSd, filename).lower()
if not os.path.exists(file) or not os.path.isfile(file):
self.readList.append("open failed, File: %s." % filename)
else:
self._selectedSdFile = file
self._selectedSdFileSize = os.stat(file).st_size
self.readList.append("File opened: %s Size: %d" % (filename, self._selectedSdFileSize))
self.readList.append("File selected")
def _startSdPrint(self):
if self._selectedSdFile is not None:
if self._sdPrinter is None:
self._sdPrinter = threading.Thread(target=self._sdPrintingWorker)
self._sdPrinter.start()
self._sdPrintingSemaphore.set()
self._sendOk()
def _pauseSdPrint(self):
self._sdPrintingSemaphore.clear()
self._sendOk()
def _setSdPos(self, pos):
self._newSdFilePos = pos
def _reportSdStatus(self):
if self._sdPrinter is not None and self._sdPrintingSemaphore.is_set:
self.readList.append("SD printing byte %d/%d" % (self._selectedSdFilePos, self._selectedSdFileSize))
else:
self.readList.append("Not SD printing")
def _writeSdFile(self, filename):
file = os.path.join(self._virtualSd, filename).lower()
if os.path.exists(file):
if os.path.isfile(file):
os.remove(file)
else:
self.readList.append("error writing to file")
self._writingToSd = True
self._selectedSdFile = file
self.readList.append("Writing to file: %s" % filename)
self._sendOk()
def _finishSdFile(self):
self._writingToSd = False
self._selectedSdFile = None
self._sendOk()
def _sdPrintingWorker(self):
self._selectedSdFilePos = 0
with open(self._selectedSdFile, "r") as f:
for line in f:
# reset position if requested by client
if self._newSdFilePos is not None:
f.seek(self._newSdFilePos)
self._newSdFilePos = None
# read current file position
self._selectedSdFilePos = f.tell()
# if we are paused, wait for unpausing
self._sdPrintingSemaphore.wait()
# set target temps
if 'M104' in line or 'M109' in line:
try:
self.targetTemp = float(re.search('S([0-9]+)', line).group(1))
except:
pass
if 'M140' in line or 'M190' in line:
try:
self.bedTargetTemp = float(re.search('S([0-9]+)', line).group(1))
except:
pass
time.sleep(0.01)
self._sdPrintingSemaphore.clear()
self._selectedSdFilePos = 0
self._sdPrinter = None
self.readList.append("Done printing file")
def _deleteSdFile(self, filename):
file = os.path.join(self._virtualSd, filename)
if os.path.exists(file) and os.path.isfile(file):
os.remove(file)
self._sendOk()
def readline(self):
if self.readList is None:
return ''
n = 0
timeDiff = self.lastTempAt - time.time()
self.lastTempAt = time.time()
if abs(self.temp - self.targetTemp) > 1:
self.temp += math.copysign(timeDiff * 10, self.targetTemp - self.temp)
if self.temp < 0:
self.temp = 0
if abs(self.bedTemp - self.bedTargetTemp) > 1:
self.bedTemp += math.copysign(timeDiff * 10, self.bedTargetTemp - self.bedTemp)
if self.bedTemp < 0:
self.bedTemp = 0
while len(self.readList) < 1:
time.sleep(0.1)
n += 1
if n == 20:
return ''
if self.readList is None:
return ''
time.sleep(0.001)
return self.readList.pop(0)
def close(self):
self.readList = None
def _sendOk(self):
if settings().getBoolean(["devel", "virtualPrinter", "okWithLinenumber"]):
self.readList.append("ok %d" % self.lastN)
else:
self.readList.append("ok")
def _sendWaitAfterTimeout(self, timeout=5):
time.sleep(timeout)
if self.readList is not None:
self.readList.append("wait")

8
requirements-bbb.txt Normal file
View File

@ -0,0 +1,8 @@
flask==0.9
werkzeug==0.8.3
tornado==3.0.2
sockjs-tornado>=1.0.0
PyYAML==3.10
Flask-Login==0.2.2
Flask-Principal==0.3.5
netaddr>=0.7.10

View File

@ -1,8 +1,10 @@
flask>=0.9
flask==0.9
werkzeug==0.8.3
tornado==3.0.2
sockjs-tornado>=1.0.0
PyYAML==3.10
Flask-Login==0.2.2
Flask-Principal==0.3.5
numpy>=1.6.2
pyserial>=2.6
tornado>=2.4.1
tornadio2>=0.0.4
PyYAML>=3.10
Flask-Login>=0.1.3
Flask-Principal>=0.3.5
netaddr>=0.7.10

6
run
View File

@ -39,6 +39,10 @@ def main():
help="Daemonize/control daemonized OctoPrint instance (only supported under Linux right now)")
parser.add_argument("--pid", action="store", type=str, dest="pidfile", default="/tmp/octoprint.pid",
help="Pidfile to use for daemonizing, defaults to /tmp/octoprint.pid")
parser.add_argument("--iknowwhatimdoing", action="store_true", dest="allowRoot",
help="Allow OctoPrint to run as user root")
args = parser.parse_args()
if args.daemon:
@ -54,7 +58,7 @@ def main():
elif "restart" == args.daemon:
daemon.restart()
else:
octoprint = Server(args.config, args.basedir, args.host, args.port, args.debug)
octoprint = Server(args.config, args.basedir, args.host, args.port, args.debug, args.allowRoot)
octoprint.run()
if __name__ == "__main__":

44
setup.py Normal file
View File

@ -0,0 +1,44 @@
# coding=utf-8
#!/usr/bin/env python
from setuptools import setup, find_packages
VERSION = "0.1.0"
def params():
name = "OctoPrint"
version = VERSION
description = "A responsive web interface for 3D printers"
long_description = open("README.md").read()
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: Web Environment",
"Framework :: Flask",
"Intended Audience :: Education",
"Intended Audience :: End Users/Desktop",
"Intended Audience :: Manufacturing",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: GNU Affero General Public License v3",
"Natural Language :: English",
"Operating System :: OS Independent",
"Programming Language :: Python :: 2.7",
"Programming Language :: JavaScript",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
"Topic :: Internet :: WWW/HTTP :: WSGI",
"Topic :: Printing",
"Topic :: System :: Networking :: Monitoring"
]
author = "Gina Häußge"
author_email = "osd@foosel.net"
url = "http://octoprint.org"
license = "AGPLv3"
packages = find_packages()
include_package_data = True
zip_safe = False
install_requires = open("requirements.txt").read().split("\n")
return locals()
setup(**params())