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 *.pyc
*.zip *.zip
*.exe *.exe
darwin-Cura-* *.iml
win32-Cura-*
linux-Cura-*
Printrun
.idea .idea
.DS_Store .DS_Store
Cura/current_profile.ini build
Cura/preferences.ini dist
cura.sh OctoPrint.egg-info
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

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) [![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 OctoPrint provides a responsive web interface for controlling a 3D printer (RepRap, Ultimaker, ...). It is Free Software
allows 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 Its website can be found at [octoprint.org](http://octoprint.org).
* 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
The intended usecase is to run OctoPrint on a single-board computer like the Raspberry Pi and a WiFi module, Reporting bugs
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.
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 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 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`. 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 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 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). 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 from werkzeug.utils import secure_filename
SUPPORTED_EXTENSIONS=["gcode", "gco"]
class GcodeManager: class GcodeManager:
def __init__(self): def __init__(self):
self._logger = logging.getLogger(__name__) self._logger = logging.getLogger(__name__)
@ -63,6 +65,8 @@ class GcodeManager:
dirty = True dirty = True
if gcode.extrusionAmount: if gcode.extrusionAmount:
analysisResult["filament"] = "%.2fm" % (gcode.extrusionAmount / 1000) analysisResult["filament"] = "%.2fm" % (gcode.extrusionAmount / 1000)
if gcode.calculateVolumeCm3():
analysisResult["filament"] += " / %.2fcm³" % gcode.calculateVolumeCm3()
dirty = True dirty = True
if dirty: if dirty:
@ -114,28 +118,44 @@ class GcodeManager:
#~~ file handling #~~ file handling
def addFile(self, file): def addFile(self, file):
if file: if not file:
absolutePath = self.getAbsolutePath(file.filename, mustExist=False) return None
if absolutePath is not None:
if file.filename in self._metadata.keys(): absolutePath = self.getAbsolutePath(file.filename, mustExist=False)
# delete existing metadata entry, since the file is going to get overwritten if absolutePath is None:
del self._metadata[file.filename] return None
self._metadataDirty = True
self._saveMetadata() basename = self._getBasicFilename(absolutePath)
file.save(absolutePath) if basename in self._metadata.keys():
self._metadataAnalyzer.addFileToQueue(os.path.basename(absolutePath)) # delete existing metadata entry, since the file is going to get overwritten
return self._getBasicFilename(absolutePath) del self._metadata[basename]
return None 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): def removeFile(self, filename):
filename = self._getBasicFilename(filename) filename = self._getBasicFilename(filename)
absolutePath = self.getAbsolutePath(filename) absolutePath = self.getAbsolutePath(filename)
if absolutePath is not None: if absolutePath is None:
os.remove(absolutePath) return
if filename in self._metadata.keys():
del self._metadata[filename] os.remove(absolutePath)
self._metadataDirty = True if filename in self._metadata.keys():
self._saveMetadata() del self._metadata[filename]
self._metadataDirty = True
self._saveMetadata()
def getAbsolutePath(self, filename, mustExist=True): def getAbsolutePath(self, filename, mustExist=True):
""" """
@ -153,7 +173,7 @@ class GcodeManager:
""" """
filename = self._getBasicFilename(filename) filename = self._getBasicFilename(filename)
if not util.isAllowedFile(filename, set(["gcode"])): if not util.isAllowedFile(filename.lower(), set(SUPPORTED_EXTENSIONS)):
return None return None
secure = os.path.join(self._uploadFolder, secure_filename(self._getBasicFilename(filename))) secure = os.path.join(self._uploadFolder, secure_filename(self._getBasicFilename(filename)))

View File

@ -8,10 +8,13 @@ import threading
import copy import copy
import os import os
#import logging, logging.config
import octoprint.util.comm as comm import octoprint.util.comm as comm
import octoprint.util as util import octoprint.util as util
from octoprint.settings import settings from octoprint.settings import settings
from octoprint.events import eventManager
def getConnectionOptions(): def getConnectionOptions():
""" """
@ -21,12 +24,16 @@ def getConnectionOptions():
"ports": comm.serialList(), "ports": comm.serialList(),
"baudrates": comm.baudrateList(), "baudrates": comm.baudrateList(),
"portPreference": settings().get(["serial", "port"]), "portPreference": settings().get(["serial", "port"]),
"baudratePreference": settings().getInt(["serial", "baudrate"]) "baudratePreference": settings().getInt(["serial", "baudrate"]),
"autoconnect": settings().getBoolean(["serial", "autoconnect"])
} }
class Printer(): class Printer():
def __init__(self, gcodeManager): def __init__(self, gcodeManager):
from collections import deque
self._gcodeManager = gcodeManager self._gcodeManager = gcodeManager
self._gcodeManager.registerCallback(self)
# state # state
self._temp = None self._temp = None
@ -34,19 +41,19 @@ class Printer():
self._targetTemp = None self._targetTemp = None
self._targetBedTemp = None self._targetBedTemp = None
self._temps = { self._temps = {
"actual": [], "actual": deque([], 300),
"target": [], "target": deque([], 300),
"actualBed": [], "actualBed": deque([], 300),
"targetBed": [] "targetBed": deque([], 300)
} }
self._tempBacklog = [] self._tempBacklog = []
self._latestMessage = None self._latestMessage = None
self._messages = [] self._messages = deque([], 300)
self._messageBacklog = [] self._messageBacklog = []
self._latestLog = None self._latestLog = None
self._log = [] self._log = deque([], 300)
self._logBacklog = [] self._logBacklog = []
self._state = None self._state = None
@ -57,16 +64,14 @@ class Printer():
self._printTime = None self._printTime = None
self._printTimeLeft = None self._printTimeLeft = None
# gcode handling self._printAfterSelect = False
self._gcodeList = None
self._filename = None
self._gcodeLoader = None
# feedrate # sd handling
self._feedrateModifierMapping = {"outerWall": "WALL-OUTER", "innerWall": "WALL_INNER", "fill": "FILL", "support": "SUPPORT"} self._sdPrinting = False
self._sdStreaming = False
self._sdFilelistAvailable = threading.Event()
# timelapse self._selectedFile = None
self._timelapse = None
# comm # comm
self._comm = None self._comm = None
@ -84,9 +89,8 @@ class Printer():
) )
self._stateMonitor.reset( self._stateMonitor.reset(
state={"state": None, "stateString": self.getStateString(), "flags": self._getStateFlags()}, state={"state": None, "stateString": self.getStateString(), "flags": self._getStateFlags()},
jobData={"filename": None, "lines": None, "estimatedPrintTime": None, "filament": None}, jobData={"filename": None, "filesize": None, "estimatedPrintTime": None, "filament": None},
gcodeData={"filename": None, "progress": None}, progress={"progress": None, "filepos": None, "printTime": None, "printTimeLeft": None},
progress={"progress": None, "printTime": None, "printTimeLeft": None},
currentZ=None currentZ=None
) )
@ -120,7 +124,25 @@ class Printer():
try: callback.sendCurrentData(copy.deepcopy(data)) try: callback.sendCurrentData(copy.deepcopy(data))
except: pass 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): def connect(self, port=None, baudrate=None):
""" """
@ -138,6 +160,7 @@ class Printer():
if self._comm is not None: if self._comm is not None:
self._comm.close() self._comm.close()
self._comm = None self._comm = None
eventManager().fire("Disconnected")
def command(self, command): def command(self, command):
""" """
@ -149,48 +172,48 @@ class Printer():
""" """
Sends multiple gcode commands (provided as a list) to the printer. Sends multiple gcode commands (provided as a list) to the printer.
""" """
if self._comm is None:
return
for command in commands: for command in commands:
self._comm.sendCommand(command) self._comm.sendCommand(command)
def setFeedrateModifier(self, structure, percentage): def setTemperatureOffset(self, extruder, bed):
if (not self._feedrateModifierMapping.has_key(structure)) or percentage < 0: if self._comm is None:
return 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): def selectFile(self, filename, sd, printAfterSelect=False):
""" if self._comm is None or (self._comm.isBusy() or self._comm.isStreaming()):
Loads the gcode from the given file as the new print job.
Aborts if the printer is currently printing or another gcode file is currently being loaded.
"""
if (self._comm is not None and self._comm.isPrinting()) or (self._gcodeLoader is not None):
return 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 def unselectFile(self):
if printAfterLoading: if self._comm is not None and (self._comm.isBusy() or self._comm.isStreaming()):
onGcodeLoadedCallback = self._onGcodeLoadedToPrint return
self._gcodeLoader = GcodeLoader(file, self._onGcodeLoadingProgress, onGcodeLoadedCallback) self._comm.unselectFile()
self._gcodeLoader.start() self._setProgressData(0, None, None, None)
self._setCurrentZ(None)
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
def startPrint(self): def startPrint(self):
""" """
Starts the currently loaded print job. Starts the currently loaded print job.
Only starts if the printer is connected and operational, not currently printing and a printjob is loaded 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 return
if self._gcodeList is None: if self._selectedFile is None:
return
if self._comm.isPrinting():
return return
self._setCurrentZ(-1) self._setCurrentZ(None)
self._comm.printGCode(self._gcodeList) self._comm.startPrint()
def togglePausePrint(self): def togglePausePrint(self):
""" """
@ -198,6 +221,7 @@ class Printer():
""" """
if self._comm is None: if self._comm is None:
return return
self._comm.setPause(not self._comm.isPaused()) self._comm.setPause(not self._comm.isPaused())
def cancelPrint(self, disableMotorsAndHeater=True): def cancelPrint(self, disableMotorsAndHeater=True):
@ -206,28 +230,23 @@ class Printer():
""" """
if self._comm is None: if self._comm is None:
return return
self._comm.cancelPrint() self._comm.cancelPrint()
if disableMotorsAndHeater: if disableMotorsAndHeater:
self.commands(["M84", "M104 S0", "M140 S0", "M106 S0"]) # disable motors, switch off heaters and fan 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._setCurrentZ(None)
self._setProgressData(None, None, None) self._setProgressData(None, None, None, None)
# mark print as failure # 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 #~~ 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): def _setCurrentZ(self, currentZ):
self._currentZ = currentZ self._currentZ = currentZ
@ -242,15 +261,13 @@ class Printer():
def _addLog(self, log): def _addLog(self, log):
self._log.append(log) self._log.append(log)
self._log = self._log[-300:]
self._stateMonitor.addLog(log) self._stateMonitor.addLog(log)
def _addMessage(self, message): def _addMessage(self, message):
self._messages.append(message) self._messages.append(message)
self._messages = self._messages[-300:]
self._stateMonitor.addMessage(message) self._stateMonitor.addMessage(message)
def _setProgressData(self, progress, printTime, printTimeLeft): def _setProgressData(self, progress, filepos, printTime, printTimeLeft):
self._progress = progress self._progress = progress
self._printTime = printTime self._printTime = printTime
self._printTimeLeft = printTimeLeft self._printTimeLeft = printTimeLeft
@ -263,22 +280,19 @@ class Printer():
if (self._printTimeLeft): if (self._printTimeLeft):
formattedPrintTimeLeft = util.getFormattedTimeDelta(datetime.timedelta(minutes=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): def _addTemperatureData(self, temp, bedTemp, targetTemp, bedTargetTemp):
currentTimeUtc = int(time.time() * 1000) currentTimeUtc = int(time.time() * 1000)
self._temps["actual"].append((currentTimeUtc, temp)) self._temps["actual"].append((currentTimeUtc, temp))
self._temps["actual"] = self._temps["actual"][-300:]
self._temps["target"].append((currentTimeUtc, targetTemp)) self._temps["target"].append((currentTimeUtc, targetTemp))
self._temps["target"] = self._temps["target"][-300:]
self._temps["actualBed"].append((currentTimeUtc, bedTemp)) self._temps["actualBed"].append((currentTimeUtc, bedTemp))
self._temps["actualBed"] = self._temps["actualBed"][-300:]
self._temps["targetBed"].append((currentTimeUtc, bedTargetTemp)) self._temps["targetBed"].append((currentTimeUtc, bedTargetTemp))
self._temps["targetBed"] = self._temps["targetBed"][-300:]
self._temp = temp self._temp = temp
self._bedTemp = bedTemp 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}) self._stateMonitor.addTemperature({"currentTime": currentTimeUtc, "temp": self._temp, "bedTemp": self._bedTemp, "targetTemp": self._targetTemp, "targetBedTemp": self._targetBedTemp})
def _setJobData(self, filename, gcodeList): def _setJobData(self, filename, filesize, sd):
self._filename = filename if filename is not None:
self._gcodeList = gcodeList self._selectedFile = {
"filename": filename,
lines = None "filesize": filesize,
if self._gcodeList: "sd": sd
lines = len(self._gcodeList) }
else:
self._selectedFile = None
formattedFilename = None formattedFilename = None
formattedFilesize = None
estimatedPrintTime = None estimatedPrintTime = None
fileMTime = None
filament = None filament = None
if self._filename: if filename:
formattedFilename = os.path.basename(self._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) fileData = self._gcodeManager.getFileData(filename)
if fileData is not None and "gcodeAnalysis" in fileData.keys(): if fileData is not None and "gcodeAnalysis" in fileData.keys():
@ -308,15 +334,17 @@ class Printer():
if "filament" in fileData["gcodeAnalysis"].keys(): if "filament" in fileData["gcodeAnalysis"].keys():
filament = fileData["gcodeAnalysis"]["filament"] 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): def _sendInitialStateUpdate(self, callback):
try: try:
data = self._stateMonitor.getCurrentData() 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({ data.update({
"temperatureHistory": self._temps, "temperatureHistory": temps,
"logHistory": self._log, "logHistory": list(self._log),
"messageHistory": self._messages "messageHistory": list(self._messages)
}) })
callback.sendHistoryData(data) callback.sendHistoryData(data)
except Exception, err: except Exception, err:
@ -325,16 +353,24 @@ class Printer():
pass pass
def _getStateFlags(self): def _getStateFlags(self):
if not settings().getBoolean(["feature", "sdSupport"]) or self._comm is None:
sdReady = False
else:
sdReady = self._comm.isSdReady()
return { return {
"operational": self.isOperational(), "operational": self.isOperational(),
"printing": self.isPrinting(), "printing": self.isPrinting(),
"closedOrError": self.isClosedOrError(), "closedOrError": self.isClosedOrError(),
"error": self.isError(), "error": self.isError(),
"loading": self.isLoading(),
"paused": self.isPaused(), "paused": self.isPaused(),
"ready": self.isReady() "ready": self.isReady(),
"sdReady": sdReady
} }
def getCurrentData(self):
return self._stateMonitor.getCurrentData()
#~~ callbacks triggered from self._comm #~~ callbacks triggered from self._comm
def mcLog(self, message): def mcLog(self, message):
@ -352,26 +388,19 @@ class Printer():
""" """
oldState = self._state 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 # forward relevant state changes to gcode manager
if self._comm is not None and oldState == self._comm.STATE_PRINTING: if self._comm is not None and oldState == self._comm.STATE_PRINTING:
if state == self._comm.STATE_OPERATIONAL: if self._selectedFile is not None:
self._gcodeManager.printSucceeded(self._filename) if state == self._comm.STATE_OPERATIONAL:
elif state == self._comm.STATE_CLOSED or state == self._comm.STATE_ERROR or state == self._comm.STATE_CLOSED_WITH_ERROR: self._gcodeManager.printSucceeded(self._selectedFile["filename"])
self._gcodeManager.printFailed(self._filename) elif state == self._comm.STATE_CLOSED or state == self._comm.STATE_ERROR or state == self._comm.STATE_CLOSED_WITH_ERROR:
self._gcodeManager.resumeAnalysis() # do not analyse gcode while printing 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: 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) self._setState(state)
def mcMessage(self, message): def mcMessage(self, message):
""" """
Callback method for the comm object, called upon message exchanges via serial. Callback method for the comm object, called upon message exchanges via serial.
@ -379,66 +408,109 @@ class Printer():
""" """
self._addMessage(message) 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. 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: self._setProgressData(self._comm.getPrintProgress(), self._comm.getPrintFilepos(), self._comm.getPrintTime(), self._comm.getPrintTimeRemainingEstimate())
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())
def mcZChange(self, newZ): def mcZChange(self, newZ):
""" """
Callback method for the comm object, called upon change of the z-layer. Callback method for the comm object, called upon change of the z-layer.
""" """
oldZ = self._currentZ oldZ = self._currentZ
if self._timelapse is not None: if newZ != oldZ:
self._timelapse.onZChange(oldZ, newZ) # 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) self._setCurrentZ(newZ)
#~~ callbacks triggered by gcodeLoader def mcSdStateChange(self, sdReady):
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})
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()}) self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
def _onGcodeLoadedToPrint(self, filename, gcodeList): def mcSdFiles(self, files):
self._onGcodeLoaded(filename, gcodeList) self._sendTriggerUpdateCallbacks("gcodeFiles")
self.startPrint() 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 #~~ 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): def getStateString(self):
""" """
Returns a human readable string corresponding to the current communication state. Returns a human readable string corresponding to the current communication state.
@ -448,6 +520,33 @@ class Printer():
else: else:
return self._comm.getStateString() 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): def isClosedOrError(self):
return self._comm is None or self._comm.isClosedOrError() 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() return self._comm is not None and self._comm.isError()
def isReady(self): 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): def isLoading(self):
return self._gcodeLoader is not None return self._gcodeLoader is not None
@ -488,7 +587,7 @@ class GcodeLoader(threading.Thread):
def run(self): def run(self):
#Send an initial M110 to reset the line counter to zero. #Send an initial M110 to reset the line counter to zero.
prevLineType = lineType = "CUSTOM" prevLineType = lineType = "CUSTOM"
gcodeList = ["M110"] gcodeList = ["M110 N0"]
filesize = os.stat(self._filename).st_size filesize = os.stat(self._filename).st_size
with open(self._filename, "r") as file: with open(self._filename, "r") as file:
for line in file: for line in file:
@ -514,6 +613,38 @@ class GcodeLoader(threading.Thread):
def _onParsingProgress(self, progress): def _onParsingProgress(self, progress):
self._progressCallback(self._filename, progress, "parsing") 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): class StateMonitor(object):
def __init__(self, ratelimit, updateCallback, addTemperatureCallback, addLogCallback, addMessageCallback): def __init__(self, ratelimit, updateCallback, addTemperatureCallback, addLogCallback, addMessageCallback):
self._ratelimit = ratelimit self._ratelimit = ratelimit
@ -525,9 +656,13 @@ class StateMonitor(object):
self._state = None self._state = None
self._jobData = None self._jobData = None
self._gcodeData = None self._gcodeData = None
self._sdUploadData = None
self._currentZ = None self._currentZ = None
self._progress = None self._progress = None
self._tempOffset = 0
self._bedTempOffset = 0
self._changeEvent = threading.Event() self._changeEvent = threading.Event()
self._lastUpdate = time.time() self._lastUpdate = time.time()
@ -535,10 +670,9 @@ class StateMonitor(object):
self._worker.daemon = True self._worker.daemon = True
self._worker.start() 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.setState(state)
self.setJobData(jobData) self.setJobData(jobData)
self.setGcodeData(gcodeData)
self.setProgress(progress) self.setProgress(progress)
self.setCurrentZ(currentZ) self.setCurrentZ(currentZ)
@ -566,14 +700,17 @@ class StateMonitor(object):
self._jobData = jobData self._jobData = jobData
self._changeEvent.set() self._changeEvent.set()
def setGcodeData(self, gcodeData):
self._gcodeData = gcodeData
self._changeEvent.set()
def setProgress(self, progress): def setProgress(self, progress):
self._progress = progress self._progress = progress
self._changeEvent.set() 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): def _work(self):
while True: while True:
self._changeEvent.wait() self._changeEvent.wait()
@ -593,8 +730,8 @@ class StateMonitor(object):
return { return {
"state": self._state, "state": self._state,
"job": self._jobData, "job": self._jobData,
"gcode": self._gcodeData,
"currentZ": self._currentZ, "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>" __author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
import ConfigParser
import sys import sys
import os import os
import yaml import yaml
import logging import logging
import re
import uuid
APPNAME="OctoPrint" APPNAME="OctoPrint"
@ -24,28 +25,48 @@ def settings(init=False, configfile=None, basedir=None):
default_settings = { default_settings = {
"serial": { "serial": {
"port": None, "port": None,
"baudrate": None "baudrate": None,
"autoconnect": False,
"log": False,
"timeout": {
"detection": 0.5,
"connection": 2,
"communication": 5
},
"additionalPorts": []
}, },
"server": { "server": {
"host": "0.0.0.0", "host": "0.0.0.0",
"port": 5000 "port": 5000,
"firstRun": True
}, },
"webcam": { "webcam": {
"stream": None, "stream": None,
"snapshot": None, "snapshot": None,
"ffmpeg": None, "ffmpeg": None,
"bitrate": "5000k", "bitrate": "5000k",
"watermark": True "watermark": True,
"flipH": False,
"flipV": False,
"timelapse": {
"type": "off",
"options": {}
}
}, },
"feature": { "feature": {
"gCodeVisualizer": True, "gCodeVisualizer": True,
"waitForStartOnConnect": False "temperatureGraph": True,
"waitForStartOnConnect": False,
"alwaysSendChecksum": False,
"sdSupport": True,
"swallowOkAfterResend": False
}, },
"folder": { "folder": {
"uploads": None, "uploads": None,
"timelapse": None, "timelapse": None,
"timelapse_tmp": None, "timelapse_tmp": None,
"logs": None "logs": None,
"virtualSd": None
}, },
"temperature": { "temperature": {
"profiles": "profiles":
@ -60,7 +81,8 @@ default_settings = {
"y": 6000, "y": 6000,
"z": 200, "z": 200,
"e": 300 "e": 300
} },
"pauseTriggers": []
}, },
"appearance": { "appearance": {
"name": "", "name": "",
@ -71,9 +93,36 @@ default_settings = {
"actions": [] "actions": []
}, },
"accessControl": { "accessControl": {
"enabled": False, "enabled": True,
"userManager": "octoprint.users.FilebasedUserManager", "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): if os.path.exists(self._configfile) and os.path.isfile(self._configfile):
with open(self._configfile, "r") as f: with open(self._configfile, "r") as f:
self._config = yaml.safe_load(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 = {} self._config = {}
def save(self, force=False): 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)) self._logger.warn("Could not convert %r to a valid integer when getting option %r" % (value, path))
return None 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): def getBoolean(self, path):
value = self.get(path) value = self.get(path)
if value is None: if value is None:
@ -199,6 +260,54 @@ class Settings(object):
return folder return folder
def getFeedbackControls(self):
feedbackControls = []
for control in self.get(["controls"]):
feedbackControls.extend(self._getFeedbackControls(control))
return feedbackControls
def _getFeedbackControls(self, control=None):
if control["type"] == "feedback_command" 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 #~~ setter
def set(self, path, value, force=False): def set(self, path, value, force=False):
@ -234,6 +343,7 @@ class Settings(object):
def setInt(self, path, value, force=False): def setInt(self, path, value, force=False):
if value is None: if value is None:
self.set(path, None, force) self.set(path, None, force)
return
try: try:
intValue = int(value) intValue = int(value)
@ -243,6 +353,19 @@ class Settings(object):
self.set(path, intValue, force) 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): def setBoolean(self, path, value, force=False):
if value is None or isinstance(value, bool): if value is None or isinstance(value, bool):
self.set(path, value, force) self.set(path, value, force)

View File

@ -5,13 +5,48 @@ body {
} }
.navbar-inner-text (@base) { .navbar-inner-text (@base) {
text-shadow: 0 1px 0 lighten(@base, 15%); text-shadow: 0 1px 0 contrast(@base, lighten(@base, 15%), darken(@base, 15%));
color: contrast(@base, #333333, #f2f2f2); 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) { .navbar-inner-color (@base) {
@top: lighten(@base, 25%); @top: lighten(@base, 25%);
@bottom: darken(@base, 15%); @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-color: @base; /* fallback color if gradients are not supported */
background-image: -webkit-linear-gradient(top, @top, @bottom); /* For Chrome and Safari */ background-image: -webkit-linear-gradient(top, @top, @bottom); /* For Chrome and Safari */
@ -23,6 +58,10 @@ body {
.brand, .nav>li>a { .brand, .nav>li>a {
.navbar-inner-text(@base); .navbar-inner-text(@base);
} }
.brand {
.brand(@text-color, @text-color-dark, @text-color-light);
}
.nav { .nav {
li.dropdown.open>.dropdown-toggle, li.dropdown.active>.dropdown-toggle, li.dropdown.open.active>.dropdown-toggle { li.dropdown.open>.dropdown-toggle, li.dropdown.active>.dropdown-toggle, li.dropdown.open.active>.dropdown-toggle {
@ -65,9 +104,16 @@ body {
@base: #7728FF; @base: #7728FF;
.navbar-inner-color(@base); .navbar-inner-color(@base);
} }
&.black {
.brand img { @base: #383838;
vertical-align: bottom; .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 { .octoprint-container {
.accordion-heading { .accordion-heading {
.settings-trigger { .accordion-heading-button {
float: right; float: right;
.dropdown-toggle { a {
display: inline-block; display: inline-block;
padding: 8px 15px; padding: 8px 15px;
font-size: 14px; font-size: 14px;
@ -145,18 +191,27 @@ body {
padding-right: 4px; padding-right: 4px;
} }
.upload-buttons .btn {
margin-right: 0;
}
/** Tables */ /** Tables */
table { table {
table-layout: fixed; table-layout: fixed;
.popover-title {
text-overflow: ellipsis;
word-break: break-all;
}
th, td { th, td {
overflow: hidden; overflow: hidden;
// gcode files // gcode files
&.gcode_files_name { &.gcode_files_name {
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap;
text-align: left; text-align: left;
} }
@ -309,6 +364,21 @@ ul.dropdown-menu li a {
#webcam_container { #webcam_container {
width: 100%; 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 */ /** GCODE file manager */
@ -321,6 +391,10 @@ ul.dropdown-menu li a {
margin-bottom: 0; margin-bottom: 0;
} }
} }
table {
margin-bottom: 0;
}
} }
/** Control tab */ /** Control tab */
@ -371,21 +445,36 @@ ul.dropdown-menu li a {
} }
/** Terminal output */
#term {
#terminal-output {
min-height: 340px;
}
}
/** Settings dialog */ /** Settings dialog */
#settings_dialog { #settings_dialog {
} }
/** Footer */ /** Footer */
.footer { .footer {
text-align: right; ul {
margin: 0;
ul li { li {
display: inline; &:first-child {
margin-left: 1em; margin-left: 0;
font-size: 85%; }
a {
color: #555; display: inline;
margin-left: 1em;
font-size: 85%;
a {
color: #555;
}
} }
} }
} }
@ -399,3 +488,133 @@ ul.dropdown-menu li a {
overflow: visible !important; 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; var assumeNonDC = false;
for(var i=0;i<gcode.length;i++){ for(var i=0;i<gcode.length;i++){
// for(var len = gcode.length- 1, i=0;i!=len;i++){
x=undefined; x=undefined;
y=undefined; y=undefined;
z=undefined; z=undefined;
retract = 0; retract = 0;
var line = gcode[i].line;
var percentage = gcode[i].percentage;
extrude=false; extrude=false;
gcode[i] = gcode[i].split(/[\(;]/)[0]; line = line.split(/[\(;]/)[0];
// prevRetract=0; if(reg.test(line)){
// retract=0; var args = line.split(/\s/);
// if(gcode[i].match(/^(?:G0|G1)\s+/i)){
if(reg.test(gcode[i])){
var args = gcode[i].split(/\s/);
for(j=0;j<args.length;j++){ for(j=0;j<args.length;j++){
// console.log(args);
// if(!args[j])continue;
switch(argChar = args[j].charAt(0).toLowerCase()){ switch(argChar = args[j].charAt(0).toLowerCase()){
case 'x': case 'x':
x=args[j].slice(1); x=args[j].slice(1);
// if(x === prevX){
// x=undefined;
// }
break; break;
case 'y': case 'y':
y=args[j].slice(1); y=args[j].slice(1);
// if(y===prevY){
// y=undefined;
// }
break; break;
case 'z': case 'z':
z=args[j].slice(1); z=args[j].slice(1);
z = Number(z); z = Number(z);
if(z == prevZ)continue; if(z == prevZ) continue;
// z = Number(z);
if(z_heights.hasOwnProperty(z)){ if(z_heights.hasOwnProperty(z)){
layer = z_heights[z]; layer = z_heights[z];
}else{ }else{
@ -267,9 +256,6 @@
} }
sendLayer = layer; sendLayer = layer;
sendLayerZ = z; sendLayerZ = z;
// if(parseFloat(prevZ) < )
// if(args[j].charAt(1) === "-")layer--;
// else layer++;
prevZ = z; prevZ = z;
break; break;
case 'e': case 'e':
@ -292,13 +278,11 @@
retract = -1; retract = -1;
} }
else if(prev_extrude["abs"]==0){ else if(prev_extrude["abs"]==0){
// if(prevRetract <0 )prevRetract=retract;
retract = 0; retract = 0;
}else if(prev_extrude["abs"]>0&&prevRetract < 0){ }else if(prev_extrude["abs"]>0&&prevRetract < 0){
prevRetract = 0; prevRetract = 0;
retract = 1; retract = 1;
} else { } else {
// prevRetract = retract;
retract = 0; retract = 0;
} }
prev_extrude[argChar] = numSlice; prev_extrude[argChar] = numSlice;
@ -317,24 +301,24 @@
prev_extrude["abs"] = Math.sqrt((prevX-x)*(prevX-x)+(prevY-y)*(prevY-y)); prev_extrude["abs"] = Math.sqrt((prevX-x)*(prevX-x)+(prevY-y)*(prevY-y));
} }
if(!model[layer])model[layer]=[]; 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}; //{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(x) !== 'undefined') prevX = x;
if(typeof(y) !== 'undefined') prevY = y; if(typeof(y) !== 'undefined') prevY = y;
} else if(gcode[i].match(/^(?:M82)/i)){ } else if(line.match(/^(?:M82)/i)){
extrudeRelative = false; extrudeRelative = false;
}else if(gcode[i].match(/^(?:G91)/i)){ }else if(line.match(/^(?:G91)/i)){
extrudeRelative=true; extrudeRelative=true;
}else if(gcode[i].match(/^(?:G90)/i)){ }else if(line.match(/^(?:G90)/i)){
extrudeRelative=false; extrudeRelative=false;
}else if(gcode[i].match(/^(?:M83)/i)){ }else if(line.match(/^(?:M83)/i)){
extrudeRelative=true; extrudeRelative=true;
}else if(gcode[i].match(/^(?:M101)/i)){ }else if(line.match(/^(?:M101)/i)){
dcExtrude=true; dcExtrude=true;
}else if(gcode[i].match(/^(?:M103)/i)){ }else if(line.match(/^(?:M103)/i)){
dcExtrude=false; dcExtrude=false;
}else if(gcode[i].match(/^(?:G92)/i)){ }else if(line.match(/^(?:G92)/i)){
var args = gcode[i].split(/\s/); var args = line.split(/\s/);
for(j=0;j<args.length;j++){ for(j=0;j<args.length;j++){
switch(argChar = args[j].charAt(0).toLowerCase()){ switch(argChar = args[j].charAt(0).toLowerCase()){
case 'x': case 'x':
@ -354,16 +338,15 @@
else { else {
prev_extrude[argChar] = numSlice; prev_extrude[argChar] = numSlice;
} }
// prevZ = z;
break; break;
default: default:
break; break;
} }
} }
if(!model[layer])model[layer]=[]; 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)}; 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(gcode[i].match(/^(?:G28)/i)){ }else if(line.match(/^(?:G28)/i)){
var args = gcode[i].split(/\s/); var args = line.split(/\s/);
for(j=0;j<args.length;j++){ for(j=0;j<args.length;j++){
switch(argChar = args[j].charAt(0).toLowerCase()){ switch(argChar = args[j].charAt(0).toLowerCase()){
case 'x': case 'x':
@ -405,17 +388,11 @@
} }
prevZ = z; 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(!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};
// 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(sendLayer) !== "undefined"){ if(typeof(sendLayer) !== "undefined"){
// sendLayerToParent(sendLayer, sendLayerZ, i/gcode.length*100);
// sendLayer = undefined;
if(i-lastSend > gcode.length*0.02 && sendMultiLayer.length != 0){ if(i-lastSend > gcode.length*0.02 && sendMultiLayer.length != 0){
lastSend = i; lastSend = i;
@ -429,13 +406,7 @@
sendLayerZ = undefined; sendLayerZ = undefined;
} }
} }
// sendMultiLayer[sendMultiLayer.length] = layer;
// sendMultiLayerZ[sendMultiLayerZ.length] = z;
sendMultiLayerToParent(sendMultiLayer, sendMultiLayerZ, i/gcode.length*100); 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, purgeEmptyLayers: true,
analyzeModel: false analyzeModel: false
}; };
var linesCmdIndex = {};
var prepareGCode = function(){ var percentageTree = undefined;
var prepareGCode = function(totalSize){
if(!lines)return; if(!lines)return;
gcode = []; gcode = [];
var i, tmp; var i, tmp, byteCount;
byteCount = 0;
for(i=0;i<lines.length;i++){ 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(";"); tmp = lines[i].indexOf(";");
if(tmp > 1 || tmp === -1) { if(tmp > 1 || tmp === -1) {
gcode.push(lines[i]); gcode.push({line: lines[i], percentage: byteCount * 100 / totalSize});
} }
} }
lines = []; lines = [];
// console.log("GCode prepared");
}; };
var sortLayers = function(){ var sortLayers = function(){
var sortedZ = []; var sortedZ = [];
var tmpModel = []; var tmpModel = [];
// var cnt = 0;
// console.log(z_heights);
for(var layer in z_heights){ for(var layer in z_heights){
sortedZ[z_heights[layer]] = layer; sortedZ[z_heights[layer]] = layer;
// cnt++;
} }
// console.log("cnt is " + cnt);
sortedZ.sort(function(a,b){ sortedZ.sort(function(a,b){
return a-b; return a-b;
}); });
// console.log(sortedZ);
// console.log(model.length);
for(var i=0;i<sortedZ.length;i++){ 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; if(typeof(z_heights[sortedZ[i]]) === 'undefined')continue;
tmpModel[i] = model[z_heights[sortedZ[i]]]; tmpModel[i] = model[z_heights[sortedZ[i]]];
} }
model = tmpModel; model = tmpModel;
// console.log(model.length);
delete tmpModel; delete tmpModel;
}; };
var prepareLinesIndex = function(){ var prepareLinesIndex = function(){
linesCmdIndex = {}; percentageTree = undefined;
for (var l in model){ for (var l in model) {
for (var i=0; i< model[l].length; i++){ for (var i=0; i< model[l].length; i++) {
linesCmdIndex[model[l][i].gcodeLine] = {layer: l, cmd: 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 purgeLayers = function(){
var purge=true; var purge=true;
@ -102,13 +118,13 @@ GCODE.gCodeReader = (function(){
return { return {
loadFile: function(reader){ loadFile: function(reader){
// console.log("loadFile");
model = []; model = [];
z_heights = []; z_heights = [];
var totalSize = reader.target.result.length;
lines = reader.target.result.split(/\n/); lines = reader.target.result.split(/\n/);
reader.target.result = null; reader.target.result = null;
prepareGCode(); prepareGCode(totalSize);
worker.postMessage({ worker.postMessage({
"cmd":"parseGCode", "cmd":"parseGCode",
@ -129,32 +145,21 @@ GCODE.gCodeReader = (function(){
} }
}, },
passDataToRenderer: function(){ passDataToRenderer: function(){
// console.log(model);
if(gCodeOptions["sortLayers"])sortLayers(); if(gCodeOptions["sortLayers"])sortLayers();
// console.log(model);
if(gCodeOptions["purgeEmptyLayers"])purgeLayers(); if(gCodeOptions["purgeEmptyLayers"])purgeLayers();
prepareLinesIndex(); prepareLinesIndex();
// console.log(model);
GCODE.renderer.doRender(model, 0); GCODE.renderer.doRender(model, 0);
// GCODE.renderer3d.setModel(model);
}, },
processLayerFromWorker: function(msg){ 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; model[msg.layerNum] = msg.cmds;
z_heights[msg.zHeightObject.zValue] = msg.zHeightObject.layer; z_heights[msg.zHeightObject.zValue] = msg.zHeightObject.layer;
// GCODE.renderer.doRender(model, msg.layerNum);
}, },
processMultiLayerFromWorker: function(msg){ processMultiLayerFromWorker: function(msg){
for(var i=0;i<msg.layerNum.length;i++){ for(var i=0;i<msg.layerNum.length;i++){
model[msg.layerNum[i]] = msg.model[msg.layerNum[i]]; model[msg.layerNum[i]] = msg.model[msg.layerNum[i]];
z_heights[msg.zHeightObject.zValue[i]] = msg.layerNum[i]; z_heights[msg.zHeightObject.zValue[i]] = msg.layerNum[i];
} }
// console.log(model);
}, },
processAnalyzeModelDone: function(msg){ processAnalyzeModelDone: function(msg){
min = msg.min; min = msg.min;
@ -190,8 +195,13 @@ GCODE.gCodeReader = (function(){
var result = {first: model[layer][fromSegments].gcodeLine, last: model[layer][toSegments].gcodeLine}; var result = {first: model[layer][fromSegments].gcodeLine, last: model[layer][toSegments].gcodeLine};
return result; return result;
}, },
getLinesCmdIndex: function(line){ getCmdIndexForPercentage: function(percentage) {
return linesCmdIndex[line]; 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_background"></div>
<div id="offline_overlay_wrapper"> <div id="offline_overlay_wrapper">
<div class="container"> <div class="container">
@ -17,6 +17,21 @@
</div> </div>
</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 id="confirmation_dialog" class="modal hide fade">
<div class="modal-header"> <div class="modal-header">
<a href="#" class="close" data-dismiss="modal" aria-hidden="true">&times;</a> <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" data-dismiss="modal" aria-hidden="true">Cancel</a>
<a href="#" class="btn btn-danger confirmation_dialog_acknowledge">Proceed</a> <a href="#" class="btn btn-danger confirmation_dialog_acknowledge">Proceed</a>
</div> </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> </div>

View File

@ -24,17 +24,20 @@
var CONFIG_USERSPERPAGE = 10; var CONFIG_USERSPERPAGE = 10;
var CONFIG_WEBCAM_STREAM = "{{ webcamStream }}"; var CONFIG_WEBCAM_STREAM = "{{ webcamStream }}";
var CONFIG_ACCESS_CONTROL = {% if enableAccessControl -%} true; {% else %} false; {%- endif %} 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 SOCKJS_URI = window.location.protocol.slice(0, -1) + "://" + (window.document ? window.document.domain : window.location.hostname) + ":" + window.location.port + "/sockjs";
var WEB_SOCKET_DEBUG = true; var SOCKJS_DEBUG = {% if debug -%} true; {% else %} false; {%- endif %}
</script> </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> </head>
<body> <body>
<div id="navbar" class="navbar navbar-fixed-top"> <div id="navbar" class="navbar navbar-fixed-top">
<div class="navbar-inner" data-bind="css: appearance.color"> <div class="navbar-inner" data-bind="css: appearance.color">
<div class="container"> <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"> <div class="nav-collapse">
<ul class="nav pull-right"> <ul class="nav pull-right">
<li style="display: none;" data-bind="visible: loginState.isAdmin"> <li style="display: none;" data-bind="visible: loginState.isAdmin">
@ -89,14 +92,17 @@
</div> </div>
<div class="accordion-body collapse in" id="connection"> <div class="accordion-body collapse in" id="connection">
<div class="accordion-inner"> <div class="accordion-inner">
<label for="connection_ports" data-bind="css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed && loginState.isUser">Serial Port</label> <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> <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> <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> <select id="connection_baudrates" data-bind="options: baudrateOptions, optionsCaption: 'AUTO', value: selectedBaudrate, css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()"></select>
<label class="checkbox"> <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> </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> </div>
</div> </div>
@ -107,16 +113,17 @@
<div class="accordion-body collapse in" id="state"> <div class="accordion-body collapse in" id="state">
<div class="accordion-inner"> <div class="accordion-inner">
Machine State: <strong data-bind="text: stateString"></strong><br> 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> Filament: <strong data-bind="text: filament"></strong><br>
Estimated Print Time: <strong data-bind="text: estimatedPrintTime"></strong><br> Estimated Print Time: <strong data-bind="text: estimatedPrintTime"></strong><br>
Line: <strong data-bind="text: lineString"></strong><br> Timelapse: <strong data-bind="text: timelapseString"></strong><br>
Height: <strong data-bind="text: currentHeight"></strong><br> Height: <strong data-bind="text: heightString"></strong><br>
Print Time: <strong data-bind="text: printTime"></strong><br> Print Time: <strong data-bind="text: printTime"></strong><br>
Print Time Left: <strong data-bind="text: printTimeLeft"></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="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>
<div class="row-fluid print-control" style="display: none;" data-bind="visible: loginState.isUser"> <div class="row-fluid print-control" style="display: none;" data-bind="visible: loginState.isUser">
@ -131,7 +138,7 @@
<div class="accordion-heading"> <div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#files"><i class="icon-list"></i> Files</a> <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="#"> <a class="dropdown-toggle" data-toggle="dropdown" href="#">
<span class="icon-wrench"></span> <span class="icon-wrench"></span>
</a> </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('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('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> <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 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> <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> </ul>
</div> </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>
<div class="accordion-body collapse in overflow_visible" id="files"> <div class="accordion-body collapse in overflow_visible" id="files">
<div class="accordion-inner"> <div class="accordion-inner">
@ -155,15 +180,18 @@
</tr> </tr>
</thead> </thead>
<tbody data-bind="foreach: listHelper.paginatedItems"> <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_name" data-bind="text: name"></td>
<td class="gcode_files_size" data-bind="text: size"></td> <td class="gcode_files_size" data-bind="text: size"></td>
<td class="gcode_files_action"> <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> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="muted text-right">
<small>Free: <span data-bind="text: freeSpace"></span></small>
</div>
<div class="pagination pagination-mini pagination-centered"> <div class="pagination pagination-mini pagination-centered">
<ul> <ul>
<li data-bind="css: {disabled: listHelper.currentPage() === 0}"><a href="#" data-bind="click: listHelper.prevPage">«</a></li> <li data-bind="css: {disabled: listHelper.currentPage() === 0}"><a href="#" data-bind="click: listHelper.prevPage">«</a></li>
@ -176,11 +204,26 @@
</ul> </ul>
</div> </div>
<div style="display: none;" data-bind="visible: loginState.isUser"> <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"> <div class="row-fluid upload-buttons">
<i class="icon-upload icon-white"></i> {% if enableSdSupport %}
<span>Upload</span> <span class="btn btn-primary fileinput-button span6" data-bind="css: {disabled: !$root.loginState.isUser()}" style="margin-bottom: 10px">
<input id="gcode_upload" type="file" name="gcode_file" class="fileinput-button" data-url="/ajax/gcodefiles/upload" data-bind="enable: loginState.isUser()"> <i class="icon-upload-alt icon-white"></i>
</span> <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 id="gcode_upload_progress" class="progress" style="width: 100%;">
<div class="bar" style="width: 0%"></div> <div class="bar" style="width: 0%"></div>
</div> </div>
@ -204,25 +247,27 @@
<div class="tab-content"> <div class="tab-content">
<div class="tab-pane active" id="temp"> <div class="tab-pane active" id="temp">
{% if enableTemperatureGraph %}
<div class="row" style="padding-left: 20px"> <div class="row" style="padding-left: 20px">
<div id="temperature-graph"></div> <div id="temperature-graph"></div>
</div> </div>
{% endif %}
<div class="row-fluid" style="margin-bottom: 20px"> <div class="row-fluid" style="margin-bottom: 20px">
<div class="form-horizontal span6"> <div class="form-horizontal span6">
<h1>Temperature</h1> <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"> <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"> <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> <span class="add-on">&deg;C</span>
</div> </div>
<div class="btn-group"> <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()"> <button class="btn dropdown-toggle" data-toggle="dropdown" data-bind="enable: isOperational() && loginState.isUser()">
<span class="caret"></span> <span class="caret"></span>
</button> </button>
@ -234,27 +279,35 @@
<!-- /ko --> <!-- /ko -->
<li class="divider"></li> <li class="divider"></li>
<li> <li>
<a href="#" data-bind="click: function() { $root.setTemp(0); }">Off</a> <a href="#" data-bind="click: function() { $root.setTempToZero(); }">Off</a>
</li> </li>
</ul> </ul>
</div> </div>
</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>
<div class="form-horizontal span6"> <div class="form-horizontal span6">
<h1>Bed Temperature</h1> <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"> <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"> <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> <span class="add-on">&deg;C</span>
</div> </div>
<div class="btn-group"> <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()"> <button class="btn dropdown-toggle" data-toggle="dropdown" data-bind="enable: isOperational() && loginState.isUser()">
<span class="caret"></span> <span class="caret"></span>
</button> </button>
@ -266,18 +319,26 @@
<!-- /ko --> <!-- /ko -->
<li class="divider"></li> <li class="divider"></li>
<li> <li>
<a href="#" data-bind="click: function(){ $root.setBedTemp(0); }">Off</a> <a href="#" data-bind="click: function(){ $root.setBedTempToZero(); }">Off</a>
</li> </li>
</ul> </ul>
</div> </div>
</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>
</div> </div>
<div class="tab-pane" id="control"> <div class="tab-pane" id="control">
{% if webcamStream %} {% if webcamStream %}
<div id="webcam_container"> <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> </div>
{% endif %} {% endif %}
@ -356,6 +417,16 @@
<button class="btn" data-bind="text: name, enable: $root.isOperational() && $root.loginState.isUser(), click: function() { $root.sendCustomCommand($data) }"></button> <button class="btn" data-bind="text: name, enable: $root.isOperational() && $root.loginState.isUser(), click: function() { $root.sendCustomCommand($data) }"></button>
</form> </form>
</script> </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"> <script type="text/html" id="customControls_parametricCommandTemplate">
<form class="form-inline"> <form class="form-inline">
<!-- ko foreach: input --> <!-- ko foreach: input -->
@ -368,6 +439,7 @@
<script type="text/html" id="customControls_emptyTemplate"><div></div></script> <script type="text/html" id="customControls_emptyTemplate"><div></div></script>
<!-- End of templates for custom controls --> <!-- End of templates for custom controls -->
</div> </div>
{% if enableGCodeVisualizer %}
<div class="tab-pane" id="gcode"> <div class="tab-pane" id="gcode">
<canvas id="canvas" width="572" height="588"></canvas> <canvas id="canvas" width="572" height="588"></canvas>
<div id="slider-vertical"></div> <div id="slider-vertical"></div>
@ -455,15 +527,21 @@
</div> </div>
</div> </div>
{% endif %}
<div class="tab-pane" id="term"> <div class="tab-pane" id="term">
<pre id="terminal-output" class="pre-scrollable"></pre> <pre id="terminal-output" class="pre-scrollable"></pre>
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" id="terminal-autoscroll" data-bind="checked: autoscrollEnabled"> Autoscroll <input type="checkbox" id="terminal-autoscroll" data-bind="checked: autoscrollEnabled"> Autoscroll
</label> </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"> <div class="input-append" style="display: none;" data-bind="visible: loginState.isUser">
<input type="text" id="terminal-command" data-bind="enable: isOperational() && loginState.isUser()"> <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="enable: isOperational() && loginState.isUser()">Send</button> <button class="btn" type="button" id="terminal-send" data-bind="click: sendCommand, enable: isOperational() && loginState.isUser()">Send</button>
</div> </div>
</div> </div>
{% if enableTimelapse %} {% if enableTimelapse %}
@ -481,13 +559,19 @@
<div id="webcam_timelapse_timedsettings" data-bind="visible: intervalInputEnabled()"> <div id="webcam_timelapse_timedsettings" data-bind="visible: intervalInputEnabled()">
<label for="webcam_timelapse_interval">Interval</label> <label for="webcam_timelapse_interval">Interval</label>
<div class="input-append"> <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> <span class="add-on">sec</span>
</div> </div>
</div> </div>
<div data-bind="visible: loginState.isAdmin">
<label class="checkbox">
<input type="checkbox" data-bind="checked: persist"> Save as default
</label>
</div>
<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>
</div> </div>
@ -508,7 +592,7 @@
<tr data-bind="attr: {title: name}"> <tr data-bind="attr: {title: name}">
<td class="timelapse_files_name" data-bind="text: name"></td> <td class="timelapse_files_name" data-bind="text: name"></td>
<td class="timelapse_files_size" data-bind="text: size"></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> </tr>
</tbody> </tbody>
</table> </table>
@ -529,7 +613,12 @@
</div> </div>
</div> </div>
<div class="footer"> <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="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/"><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> <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 'settings.jinja2' %}
{% include 'dialogs.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/lib/jquery/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/lib/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/lib/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/lib/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/lib/avltree.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/lib/bootstrap/bootstrap.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/lib/bootstrap/bootstrap-modalmanager.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/lib/bootstrap/bootstrap-modal.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/lib/jquery/jquery.ui.core.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/lib/jquery/jquery.ui.widget.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/lib/jquery/jquery.ui.mouse.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/lib/jquery/jquery.ui.slider.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/lib/jquery/jquery.pnotify.min.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/lib/jquery/jquery.flot.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/lib/jquery/jquery.iframe-transport.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/lib/jquery/jquery.fileupload.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/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/ui.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='gcodeviewer/js/gCodeReader.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="modal-body">
<div class="tabbable"> <div class="tabbable">
<ul class="nav nav-list span4" id="settingsTabs"> <ul class="nav nav-list span4" id="settingsTabs">
<li class="active"><a href="#settings_printerParameters" data-toggle="tab">Printer Parameters</a></li> <li class="nav-header">Printer</li>
<li><a href="#settings_webcam" data-toggle="tab">Webcam</a></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_features" data-toggle="tab">Features</a></li>
<li><a href="#settings_folder" data-toggle="tab">Folder</a></li> <li><a href="#settings_webcam" data-toggle="tab">Webcam</a></li>
<li><a href="#settings_temperature" data-toggle="tab">Temperature</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> <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> </ul>
<div class="tab-content span8"> <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"> <form class="form-horizontal">
<div class="control-group"> <div class="control-group">
<label class="control-label" for="settings-movementSpeedX">Movement Speed X Axis</label> <label class="control-label" for="settings-movementSpeedX">Movement Speed X Axis</label>
@ -89,10 +152,29 @@
</label> </label>
</div> </div>
</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> </form>
</div> </div>
<div class="tab-pane" id="settings_features"> <div class="tab-pane" id="settings_features">
<form class="form-horizontal"> <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="control-group">
<div class="controls"> <div class="controls">
<label class="checkbox"> <label class="checkbox">
@ -103,7 +185,28 @@
<div class="control-group"> <div class="control-group">
<div class="controls"> <div class="controls">
<label class="checkbox"> <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> </label>
</div> </div>
</div> </div>
@ -168,6 +271,32 @@
</div> </div>
</form> </form>
</div> </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"> <div class="tab-pane" id="settings_appearance">
<form class="form-horizontal"> <form class="form-horizontal">
<div class="control-group"> <div class="control-group">
@ -185,6 +314,23 @@
</div> </div>
</form> </form>
</div> </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 %} {% if enableAccessControl %}
<div class="tab-pane" id="settings_users"> <div class="tab-pane" id="settings_users">

View File

@ -1,12 +1,9 @@
# coding=utf-8 # coding=utf-8
import logging
__author__ = "Gina Häußge <osd@foosel.net>" __author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
from octoprint.settings import settings import logging
import octoprint.util as util
import os import os
import threading import threading
import urllib import urllib
@ -14,9 +11,13 @@ import time
import subprocess import subprocess
import fnmatch import fnmatch
import datetime import datetime
import sys import sys
import octoprint.util as util
from octoprint.settings import settings
from octoprint.events import eventManager
def getFinishedTimelapses(): def getFinishedTimelapses():
files = [] files = []
basedir = settings().getBaseFolder("timelapse") basedir = settings().getBaseFolder("timelapse")
@ -32,10 +33,29 @@ def getFinishedTimelapses():
}) })
return files 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): class Timelapse(object):
def __init__(self): def __init__(self):
self._logger = logging.getLogger(__name__) self._logger = logging.getLogger(__name__)
self._imageNumber = None self._imageNumber = None
self._inTimelapse = False self._inTimelapse = False
self._gcodeFile = None self._gcodeFile = None
@ -47,17 +67,66 @@ class Timelapse(object):
self._renderThread = None self._renderThread = None
self._captureMutex = threading.Lock() self._captureMutex = threading.Lock()
def onPrintjobStarted(self, gcodeFile): # subscribe events
self.startTimelapse(gcodeFile) 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() self.stopTimelapse()
def onPrintjobProgress(self, oldPos, newPos, percentage): def onPrintResumed(self, event, payload):
pass """
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): def eventSubscriptions(self):
pass """
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): def startTimelapse(self, gcodeFile):
self._logger.debug("Starting timelapse for %s" % gcodeFile) self._logger.debug("Starting timelapse for %s" % gcodeFile)
@ -67,11 +136,13 @@ class Timelapse(object):
self._inTimelapse = True self._inTimelapse = True
self._gcodeFile = os.path.basename(gcodeFile) self._gcodeFile = os.path.basename(gcodeFile)
def stopTimelapse(self): def stopTimelapse(self, doCreateMovie=True):
self._logger.debug("Stopping timelapse") self._logger.debug("Stopping timelapse")
self._renderThread = threading.Thread(target=self._createMovie)
self._renderThread.daemon = True if doCreateMovie:
self._renderThread.start() self._renderThread = threading.Thread(target=self._createMovie)
self._renderThread.daemon = True
self._renderThread.start()
self._imageNumber = None self._imageNumber = None
self._inTimelapse = False self._inTimelapse = False
@ -85,20 +156,21 @@ class Timelapse(object):
filename = os.path.join(self._captureDir, "tmp_%05d.jpg" % (self._imageNumber)) filename = os.path.join(self._captureDir, "tmp_%05d.jpg" % (self._imageNumber))
self._imageNumber += 1 self._imageNumber += 1
self._logger.debug("Capturing image to %s" % filename) self._logger.debug("Capturing image to %s" % filename)
captureThread = threading.Thread(target=self._captureWorker, kwargs={"filename": filename}) captureThread = threading.Thread(target=self._captureWorker, kwargs={"filename": filename})
captureThread.daemon = True captureThread.daemon = True
captureThread.start() captureThread.start()
def _captureWorker(self, filename): def _captureWorker(self, filename):
eventManager().fire("CaptureStart", filename);
urllib.urlretrieve(self._snapshotUrl, filename) urllib.urlretrieve(self._snapshotUrl, filename)
self._logger.debug("Image %s captured from %s" % (filename, self._snapshotUrl)) self._logger.debug("Image %s captured from %s" % (filename, self._snapshotUrl))
eventManager().fire("CaptureDone", filename);
def _createMovie(self): def _createMovie(self):
ffmpeg = settings().get(["webcam", "ffmpeg"]) ffmpeg = settings().get(["webcam", "ffmpeg"])
bitrate = settings().get(["webcam", "bitrate"]) bitrate = settings().get(["webcam", "bitrate"])
if ffmpeg is None or bitrate is None: 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 return
input = os.path.join(self._captureDir, "tmp_%05d.jpg") 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, ffmpeg, '-i', input, '-vcodec', 'mpeg2video', '-pix_fmt', 'yuv420p', '-r', '25', '-y', '-b:v', bitrate,
'-f', 'vob'] '-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 # add watermark if configured
watermarkFilter = None
if settings().getBoolean(["webcam", "watermark"]): if settings().getBoolean(["webcam", "watermark"]):
watermark = os.path.join(os.path.dirname(__file__), "static", "img", "watermark.png") watermark = os.path.join(os.path.dirname(__file__), "static", "img", "watermark.png")
if sys.platform == "win32": if sys.platform == "win32":
# Because ffmpeg hiccups on windows' drive letters and backslashes we have to give the watermark # 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... # path a special treatment. Yeah, I couldn't believe it either...
watermark = watermark.replace("\\", "/").replace(":", "\\\\:") 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 # finalize command with output file
command.append(output)
subprocess.call(command)
self._logger.debug("Rendering movie to %s" % output) 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): def cleanCaptureDir(self):
if not os.path.isdir(self._captureDir): if not os.path.isdir(self._captureDir):
@ -138,35 +237,53 @@ class ZTimelapse(Timelapse):
Timelapse.__init__(self) Timelapse.__init__(self)
self._logger.debug("ZTimelapse initialized") self._logger.debug("ZTimelapse initialized")
def onZChange(self, oldZ, newZ): def eventSubscriptions(self):
self._logger.debug("Z change detected, capturing image") return [
("ZChange", self._onZChange)
]
def configData(self):
return {
"type": "zchange"
}
def _onZChange(self, event, payload):
self.captureImage() self.captureImage()
class TimedTimelapse(Timelapse): class TimedTimelapse(Timelapse):
def __init__(self, interval=1): def __init__(self, interval=1):
Timelapse.__init__(self) Timelapse.__init__(self)
self._interval = interval self._interval = interval
if self._interval < 1: if self._interval < 1:
self._interval = 1 # force minimum interval of 1s self._interval = 1 # force minimum interval of 1s
self._timerThread = None self._timerThread = None
self._logger.debug("TimedTimelapse initialized") self._logger.debug("TimedTimelapse initialized")
def interval(self): def interval(self):
return self._interval return self._interval
def onPrintjobStarted(self, filename): def configData(self):
Timelapse.onPrintjobStarted(self, filename) return {
"type": "timed",
"options": {
"interval": self._interval
}
}
def onPrintStarted(self, event, payload):
Timelapse.onPrintStarted(self, event, payload)
if self._timerThread is not None: if self._timerThread is not None:
return return
self._timerThread = threading.Thread(target=self.timerWorker) self._timerThread = threading.Thread(target=self._timerWorker)
self._timerThread.daemon = True self._timerThread.daemon = True
self._timerThread.start() 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") self._logger.debug("Starting timer for interval based timelapse")
while self._inTimelapse: while self._inTimelapse:
self.captureImage() self.captureImage()

View File

@ -45,6 +45,8 @@ class UserManager(object):
def getAllUsers(self): def getAllUsers(self):
return [] return []
def hasBeenCustomized(self):
return False
class HackerspaceUserManager(UserManager): class HackerspaceUserManager(UserManager):
"""A user manager for the Warsaw Hackerspace, uses interal apis.""" """A user manager for the Warsaw Hackerspace, uses interal apis."""
@ -77,17 +79,19 @@ class FilebasedUserManager(UserManager):
self._users = {} self._users = {}
self._dirty = False self._dirty = False
self._customized = None
self._load() self._load()
def _load(self): def _load(self):
if os.path.exists(self._userfile) and os.path.isfile(self._userfile): if os.path.exists(self._userfile) and os.path.isfile(self._userfile):
self._customized = True
with open(self._userfile, "r") as f: with open(self._userfile, "r") as f:
data = yaml.safe_load(f) data = yaml.safe_load(f)
for name in data.keys(): for name in data.keys():
attributes = data[name] attributes = data[name]
self._users[name] = User(name, attributes["password"], attributes["active"], attributes["roles"]) self._users[name] = User(name, attributes["password"], attributes["active"], attributes["roles"])
else: else:
self._users["admin"] = User("admin", "7557160613d5258f883014a7c3c0428de53040fc152b1791f1cc04a62b428c0c2a9c46ed330cdce9689353ab7a5352ba2b2ceb459b96e9c8ed7d0cb0b2c0c076", True, ["user", "admin"]) self._customized = False
def _save(self, force=False): def _save(self, force=False):
if not self._dirty and not force: if not self._dirty and not force:
@ -189,6 +193,9 @@ class FilebasedUserManager(UserManager):
def getAllUsers(self): def getAllUsers(self):
return map(lambda x: x.asDict(), self._users.values()) return map(lambda x: x.asDict(), self._users.values())
def hasBeenCustomized(self):
return self._customized
##~~ Exceptions ##~~ Exceptions
class UserAlreadyExists(Exception): 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: slightly reorganized and adapted. The mapping to the original Cura source is the following:
* avr_isp.* => Cura.avr_isp.* * avr_isp.* => Cura.avr_isp.*
* comm => Cura.util.machineCom * comm => Cura.util.machineCom (highly modified now)
* gcodeInterpreter => Cura.util.gcodeInterpreter * gcodeInterpreter => Cura.util.gcodeInterpreter
* util3d => Cura.util.util3d

View File

@ -2,9 +2,17 @@
__author__ = "Gina Häußge <osd@foosel.net>" __author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' __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): 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"]: for x in ["bytes","KB","MB","GB"]:
if num < 1024.0: if num < 1024.0:
@ -12,9 +20,11 @@ def getFormattedSize(num):
num /= 1024.0 num /= 1024.0
return "%3.1f%s" % (num, "TB") return "%3.1f%s" % (num, "TB")
def isAllowedFile(filename, extensions): def isAllowedFile(filename, extensions):
return "." in filename and filename.rsplit(".", 1)[1] in extensions return "." in filename and filename.rsplit(".", 1)[1] in extensions
def getFormattedTimeDelta(d): def getFormattedTimeDelta(d):
if d is None: if d is None:
return None return None
@ -23,19 +33,108 @@ def getFormattedTimeDelta(d):
seconds = d.seconds % 60 seconds = d.seconds % 60
return "%02d:%02d:%02d" % (hours, minutes, seconds) return "%02d:%02d:%02d" % (hours, minutes, seconds)
def getFormattedDateTime(d): def getFormattedDateTime(d):
if d is None: if d is None:
return None return None
return d.strftime("%Y-%m-%d %H:%M") return d.strftime("%Y-%m-%d %H:%M")
def getClass(name): def getClass(name):
""" """
Taken from http://stackoverflow.com/a/452981/2028598 Taken from http://stackoverflow.com/a/452981/2028598
""" """
parts = name.split(".") parts = name.split(".")
module = ".".join(parts[:-1]) module = ".".join(parts[:-1])
m = __import__(module) m = __import__(module)
for comp in parts[1:]: for comp in parts[1:]:
m = getattr(m, comp) 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 from __future__ import absolute_import
__copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
import sys
import math import math
import re
import os import os
import base64
import zlib
import logging
from octoprint.util import util3d from octoprint.settings import settings
preferences = { preferences = {
"extruder_offset_x1": -22.0, "extruder_offset_x1": -22.0,
@ -25,282 +27,208 @@ def getPreference(key, default=None):
class AnalysisAborted(Exception): class AnalysisAborted(Exception):
pass 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): class gcode(object):
def __init__(self): def __init__(self):
self.regMatch = {} self._logger = logging.getLogger(__name__)
self.layerList = []
self.layerList = None
self.extrusionAmount = 0 self.extrusionAmount = 0
self.totalMoveTimeMinute = 0 self.totalMoveTimeMinute = 0
self.filename = None
self.progressCallback = None self.progressCallback = None
self._abort = False self._abort = False
self._filamentDiameter = 0
def load(self, filename): def load(self, filename):
if os.path.isfile(filename): if os.path.isfile(filename):
self.filename = filename
self._fileSize = os.stat(filename).st_size self._fileSize = os.stat(filename).st_size
gcodeFile = open(filename, 'r') with open(filename, "r") as f:
self._load(gcodeFile) self._load(f)
gcodeFile.close()
def loadList(self, l):
self._load(l)
def abort(self): def abort(self):
self._abort = True self._abort = True
def calculateVolumeCm3(self):
radius = self._filamentDiameter / 2
return (self.extrusionAmount * (math.pi * radius * radius)) / 1000
def _load(self, gcodeFile): def _load(self, gcodeFile):
filePos = 0 filePos = 0
pos = util3d.Vector3() pos = [0.0, 0.0, 0.0]
posOffset = util3d.Vector3() posOffset = [0.0, 0.0, 0.0]
currentE = 0.0 currentE = 0.0
totalExtrusion = 0.0 totalExtrusion = 0.0
maxExtrusion = 0.0 maxExtrusion = 0.0
currentExtruder = 0 currentExtruder = 0
extrudeAmountMultiply = 1.0
totalMoveTimeMinute = 0.0 totalMoveTimeMinute = 0.0
absoluteE = True
scale = 1.0 scale = 1.0
posAbs = True posAbs = True
posAbsExtruder = True; feedRateXY = settings().getFloat(["printerParameters", "movementSpeed", "x"])
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)
for line in gcodeFile: for line in gcodeFile:
if self._abort: if self._abort:
raise AnalysisAborted() raise AnalysisAborted()
if type(line) is tuple: filePos += 1
line = line[0]
if self.progressCallback != None: try:
if isinstance(gcodeFile, (file)): if self.progressCallback is not None and (filePos % 1000 == 0):
self.progressCallback(float(filePos) / float(self._fileSize)) if isinstance(gcodeFile, (file)):
elif isinstance(gcodeFile, (list)): self.progressCallback(float(gcodeFile.tell()) / float(self._fileSize))
self.progressCallback(float(filePos) / float(len(gcodeFile))) elif isinstance(gcodeFile, (list)):
filePos += 1 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: if ';' in line:
#Slic3r GCode comment parser
comment = line[line.find(';')+1:].strip() comment = line[line.find(';')+1:].strip()
if comment == 'fill': if comment.startswith("filament_diameter"):
pathType = 'FILL' self._filamentDiameter = float(comment.split("=", 1)[1].strip())
elif comment == 'perimeter': elif comment.startswith("CURA_PROFILE_STRING"):
pathType = 'WALL-INNER' curaOptions = self._parseCuraProfileString(comment)
elif comment == 'skirt': if "filament_diameter" in curaOptions:
pathType = 'SKIRT' try:
if comment.startswith('LAYER:'): self._filamentDiameter = float(curaOptions["filament_diameter"])
self.layerList.append(currentLayer) except:
currentLayer = [] self._filamentDiameter = 0.0
if pathType != "CUSTOM":
startCodeDone = True
line = line[0:line.find(';')] line = line[0:line.find(';')]
T = self.getCodeInt(line, 'T')
T = getCodeInt(line, 'T')
if T is not None: if T is not None:
if currentExtruder > 0: if currentExtruder > 0:
posOffset.x -= getPreference('extruder_offset_x%d' % (currentExtruder), 0.0) posOffset[0] -= getPreference('extruder_offset_x%d' % (currentExtruder), 0.0)
posOffset.y -= getPreference('extruder_offset_y%d' % (currentExtruder), 0.0) posOffset[1] -= getPreference('extruder_offset_y%d' % (currentExtruder), 0.0)
currentExtruder = T currentExtruder = T
if currentExtruder > 0: if currentExtruder > 0:
posOffset.x += getPreference('extruder_offset_x%d' % (currentExtruder), 0.0) posOffset[0] += getPreference('extruder_offset_x%d' % (currentExtruder), 0.0)
posOffset.y += getPreference('extruder_offset_y%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 is not None:
if G == 0 or G == 1: #Move if G == 0 or G == 1: #Move
x = self.getCodeFloat(line, 'X') x = getCodeFloat(line, 'X')
y = self.getCodeFloat(line, 'Y') y = getCodeFloat(line, 'Y')
z = self.getCodeFloat(line, 'Z') z = getCodeFloat(line, 'Z')
e = self.getCodeFloat(line, 'E') e = getCodeFloat(line, 'E')
f = self.getCodeFloat(line, 'F') f = getCodeFloat(line, 'F')
oldPos = pos.copy() oldPos = pos
if x is not None: pos = pos[:]
if posAbs: if posAbs:
pos.x = x * scale + posOffset.x if x is not None:
else: pos[0] = x * scale + posOffset[0]
pos.x += x * scale if y is not None:
if y is not None: pos[1] = y * scale + posOffset[1]
if posAbs: if z is not None:
pos.y = y * scale + posOffset.y pos[2] = z * scale + posOffset[2]
else: else:
pos.y += y * scale if x is not None:
if z is not None: pos[0] += x * scale
if posAbs: if y is not None:
pos.z = z * scale + posOffset.z pos[1] += y * scale
else: if z is not None:
pos.z += z * scale pos[2] += z * scale
if f is not None: if f is not None:
feedRate = f feedRateXY = f
if x is not None or y is not None or z is not None: 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' moveType = 'move'
if e is not None: if e is not None:
if posAbsExtruder: if absoluteE:
if e > currentE: e -= currentE
moveType = 'extrude' if e > 0.0:
if e < currentE: moveType = 'extrude'
moveType = 'retract' if e < 0.0:
totalExtrusion += e - currentE moveType = 'retract'
currentE = e totalExtrusion += e
else: currentE += e
if e > 0:
moveType = 'extrude'
if e < 0:
moveType = 'retract'
totalExtrusion += e
currentE += e
if totalExtrusion > maxExtrusion: if totalExtrusion > maxExtrusion:
maxExtrusion = totalExtrusion maxExtrusion = totalExtrusion
if moveType == 'move' and oldPos.z != pos.z: else:
if oldPos.z > pos.z and abs(oldPos.z - pos.z) > 5.0 and pos.z < 1.0: e = 0.0
oldPos.z = 0.0 if moveType == 'move' and oldPos[2] != pos[2]:
layerThickness = abs(oldPos.z - pos.z) if oldPos[2] > pos[2] and abs(oldPos[2] - pos[2]) > 5.0 and pos[2] < 1.0:
if currentPath.type != moveType or currentPath.pathType != pathType: oldPos[2] = 0.0
currentPath = gcodePath(moveType, pathType, layerThickness, currentPath.list[-1])
currentLayer.append(currentPath)
newPos = pos.copy()
newPos.e = totalExtrusion
newPos.extrudeAmountMultiply = extrudeAmountMultiply
currentPath.list.append(newPos)
elif G == 4: #Delay elif G == 4: #Delay
S = self.getCodeFloat(line, 'S') S = getCodeFloat(line, 'S')
if S is not None: if S is not None:
totalMoveTimeMinute += S / 60 totalMoveTimeMinute += S / 60.0
P = self.getCodeFloat(line, 'P') P = getCodeFloat(line, 'P')
if P is not None: if P is not None:
totalMoveTimeMinute += P / 60 / 1000 totalMoveTimeMinute += P / 60.0 / 1000.0
elif G == 20: #Units are inches elif G == 20: #Units are inches
scale = 25.4 scale = 25.4
elif G == 21: #Units are mm elif G == 21: #Units are mm
scale = 1.0 scale = 1.0
elif G == 28: #Home elif G == 28: #Home
x = self.getCodeFloat(line, 'X') x = getCodeFloat(line, 'X')
y = self.getCodeFloat(line, 'Y') y = getCodeFloat(line, 'Y')
z = self.getCodeFloat(line, 'Z') z = getCodeFloat(line, 'Z')
center = [0.0,0.0,0.0]
if x is None and y is None and z is None: if x is None and y is None and z is None:
pos = util3d.Vector3() pos = center
else: else:
pos = pos[:]
if x is not None: if x is not None:
pos.x = 0.0 pos[0] = center[0]
if y is not None: if y is not None:
pos.y = 0.0 pos[1] = center[1]
if z is not None: if z is not None:
pos.z = 0.0 pos[2] = center[2]
elif G == 90: #Absolute position elif G == 90: #Absolute position
posAbs = True posAbs = True
posAbsExtruder = True
elif G == 91: #Relative position elif G == 91: #Relative position
posAbs = False posAbs = False
posAbsExtruder = False
elif G == 92: elif G == 92:
x = self.getCodeFloat(line, 'X') x = getCodeFloat(line, 'X')
y = self.getCodeFloat(line, 'Y') y = getCodeFloat(line, 'Y')
z = self.getCodeFloat(line, 'Z') z = getCodeFloat(line, 'Z')
e = self.getCodeFloat(line, 'E') e = getCodeFloat(line, 'E')
if e is not None: if e is not None:
currentE = e currentE = e
if x is not None: if x is not None:
posOffset.x = pos.x - x posOffset[0] = pos[0] - x
if y is not None: if y is not None:
posOffset.y = pos.y - y posOffset[1] = pos[1] - y
if z is not None: if z is not None:
posOffset.z = pos.z - z posOffset[2] = pos[2] - z
else:
if G not in unknownGcodes:
print "Unknown G code:" + str(G)
unknownGcodes[G] = True
else: else:
M = self.getCodeInt(line, 'M') M = getCodeInt(line, 'M')
if M is not None: if M is not None:
if M == 1: #Message with possible wait (ignored) if M == 82: #Absolute E
pass absoluteE = True
elif M == 80: #Enable power supply elif M == 83: #Relative E
pass absoluteE = False
elif M == 81: #Suicide/disable power supply if self.progressCallback is not None:
pass self.progressCallback(100.0)
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)
self.extrusionAmount = maxExtrusion self.extrusionAmount = maxExtrusion
self.totalMoveTimeMinute = totalMoveTimeMinute self.totalMoveTimeMinute = totalMoveTimeMinute
def getCodeInt(self, line, code): def _parseCuraProfileString(self, comment):
if code not in self.regMatch: return {key: value for (key, value) in map(lambda x: x.split("=", 1), zlib.decompress(base64.b64decode(comment[len("CURA_PROFILE_STRING:"):])).split("\b"))}
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 getCodeFloat(self, line, code): def getCodeInt(line, code):
if code not in self.regMatch: n = line.find(code) + 1
self.regMatch[code] = re.compile(code + '([^\s]+)') if n < 1:
m = self.regMatch[code].search(line) return None
if m == None: m = line.find(' ', n)
return None try:
try: if m < 0:
return float(m.group(1)) return int(line[n:])
except: return int(line[n:m])
return None except:
return None
if __name__ == '__main__':
for filename in sys.argv[1:]:
gcode().load(filename)
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 numpy>=1.6.2
pyserial>=2.6 pyserial>=2.6
tornado>=2.4.1 netaddr>=0.7.10
tornadio2>=0.0.4
PyYAML>=3.10
Flask-Login>=0.1.3
Flask-Principal>=0.3.5

6
run
View File

@ -39,6 +39,10 @@ def main():
help="Daemonize/control daemonized OctoPrint instance (only supported under Linux right now)") 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", 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") 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() args = parser.parse_args()
if args.daemon: if args.daemon:
@ -54,7 +58,7 @@ def main():
elif "restart" == args.daemon: elif "restart" == args.daemon:
daemon.restart() daemon.restart()
else: 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() octoprint.run()
if __name__ == "__main__": 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())