commit
627aad1954
|
@ -4,24 +4,9 @@
|
|||
*.pyc
|
||||
*.zip
|
||||
*.exe
|
||||
darwin-Cura-*
|
||||
win32-Cura-*
|
||||
linux-Cura-*
|
||||
Printrun
|
||||
*.iml
|
||||
.idea
|
||||
.DS_Store
|
||||
Cura/current_profile.ini
|
||||
Cura/preferences.ini
|
||||
cura.sh
|
||||
pypy
|
||||
python
|
||||
printrun.bat
|
||||
cura.bat
|
||||
object-mirror.png
|
||||
object.png
|
||||
*darwin.dmg
|
||||
scripts/darwin/dist/*
|
||||
scripts/darwin/build/*
|
||||
scripts/darwin/Cura.dmg.sparseimage
|
||||
scripts/win32/dist/*
|
||||
log.txt
|
||||
build
|
||||
dist
|
||||
OctoPrint.egg-info
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
recursive-include octoprint/static *
|
||||
recursive-include octoprint/templates *
|
75
README.md
75
README.md
|
@ -3,26 +3,23 @@ OctoPrint
|
|||
|
||||
[![Flattr this git repo](http://api.flattr.com/button/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=foosel&url=https://github.com/foosel/OctoPrint&title=OctoPrint&language=&tags=github&category=software)
|
||||
|
||||
OctoPrint provides a responsive web interface for controlling a 3D printer (RepRap, Ultimaker, ...). It currently
|
||||
allows
|
||||
OctoPrint provides a responsive web interface for controlling a 3D printer (RepRap, Ultimaker, ...). It is Free Software
|
||||
and released under the [GNU Affero General Public License V3](http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
* uploading .gcode files to the server and managing them via the UI
|
||||
* selecting a file for printing, getting the usual stats regarding filament length etc (stats can be disabled for
|
||||
faster initial processing)
|
||||
* starting, pausing and canceling a print job
|
||||
* while connected to the printer, gaining information regarding the current temperature of both head and bed (if available) in a nice shiny javascript-y temperature graph
|
||||
* while printing, gaining information regarding the current progress of the print job (height, percentage etc)
|
||||
* reading the communication log and send arbitrary codes to be executed by the printer
|
||||
* moving the X, Y and Z axis (jog controls), extruding, retracting and custom controls
|
||||
* optional: previewing the GCODE of the selected model to print (via gCodeVisualizer), including rendering of the progress during printing
|
||||
* optional: visual monitoring of the printer via webcam stream integrated into the UI (using e.g. MJPG-Streamer)
|
||||
* optional: creation of timelapse recordings of the printjob via webcam stream (using e.g. MJPG-Streamer) -- currently two timelaspe methods are implemented, triggering a shot on z-layer change or every "n" seconds
|
||||
Its website can be found at [octoprint.org](http://octoprint.org).
|
||||
|
||||
The intended usecase is to run OctoPrint on a single-board computer like the Raspberry Pi and a WiFi module,
|
||||
connect the printer to the server and therefore create a WiFi-enabled 3D printer. If you want to add a webcam for visual
|
||||
monitoring and timelapse support, you'll need a **powered** USB hub.
|
||||
Reporting bugs
|
||||
--------------
|
||||
|
||||
OctoPrint is Free Software and released under the [GNU Affero General Public License V3](http://www.gnu.org/licenses/agpl.html).
|
||||
OctoPrint's issue tracker can be found [on Github](https://github.com/foosel/OctoPrint/issues). **Before opening a new
|
||||
ticket please take a look at [this guide on how to file a bug report with OctoPrint](https://github.com/foosel/OctoPrint/wiki/How-to-file-a-bug-report).**
|
||||
|
||||
Sending pull requests
|
||||
---------------------
|
||||
|
||||
Please create all pull requests against the [devel branch](https://github.com/foosel/OctoPrint/tree/devel) of OctoPrint, as that one is used for developing new
|
||||
features and then merged against master when those features are deemed mature enough for general consumption. In case
|
||||
of bug fixes I'll take care to cherry pick them against master if the bugs they are fixing are critical.
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
|
@ -54,7 +51,7 @@ Alternatively, the host and port on which to bind can be defined via the configu
|
|||
|
||||
If you want to run OctoPrint as a daemon (only supported on Linux), use
|
||||
|
||||
./run --daemon {start|stop|restart} [--pid PIDFILE]
|
||||
./run --daemon {start|stop|restart} [--pid PIDFILE]
|
||||
|
||||
If you do not supply a custom pidfile location via `--pid PIDFILE`, it will be created at `/tmp/octoprint.pid`.
|
||||
|
||||
|
@ -68,46 +65,14 @@ See `run --help` for further information.
|
|||
Configuration
|
||||
-------------
|
||||
|
||||
If not specified via the commandline, the configfile `config.yaml` for OctoPrint is expected in the settings folder, which is located at ~/.octoprint on Linux, at %APPDATA%/OctoPrint on Windows and at ~/Library/Application Support/OctoPrint on MacOS.
|
||||
If not specified via the commandline, the configfile `config.yaml` for OctoPrint is expected in the settings folder,
|
||||
which is located at `~/.octoprint` on Linux, at `%APPDATA%/OctoPrint` on Windows and
|
||||
at `~/Library/Application Support/OctoPrint` on MacOS.
|
||||
|
||||
A comprehensive overview of all available configuration settings can be found [on the wiki](https://github.com/foosel/OctoPrint/wiki/Configuration).
|
||||
A comprehensive overview of all available configuration settings can be found
|
||||
[on the wiki](https://github.com/foosel/OctoPrint/wiki/Configuration).
|
||||
|
||||
Setup on a Raspberry Pi running Raspbian
|
||||
----------------------------------------
|
||||
|
||||
A comprehensive setup guide can be found [on the wiki](https://github.com/foosel/OctoPrint/wiki/Setup-on-a-Raspberry-Pi-running-Raspbian).
|
||||
|
||||
Credits
|
||||
-------
|
||||
|
||||
OctoPrint started out as a fork of Cura (https://github.com/daid/Cura) for adding a web interface to its
|
||||
printing functionality and was originally named "Printer WebUI". It still uses Cura's communication code for talking to
|
||||
the printer, but has been reorganized to only include those parts of Cura necessary for its targeted use case.
|
||||
|
||||
It also uses the following libraries and frameworks for backend and frontend:
|
||||
|
||||
* Flask: http://flask.pocoo.org/
|
||||
* Tornado: http://www.tornadoweb.org/
|
||||
* Tornadio2: https://github.com/MrJoes/tornadio2
|
||||
* PyYAML: http://pyyaml.org/
|
||||
* Socket.io: http://socket.io/
|
||||
* jQuery: http://jquery.com/
|
||||
* Bootstrap: http://twitter.github.com/bootstrap/
|
||||
* Font Awesome: http://fortawesome.github.com/Font-Awesome/
|
||||
* Knockout.js: http://knockoutjs.com/
|
||||
* Underscore.js: http://underscorejs.org/
|
||||
* Flot: http://www.flotcharts.org/
|
||||
* jQuery File Upload: http://blueimp.github.com/jQuery-File-Upload/
|
||||
* Pines Notify: http://pinesframework.org/pnotify/
|
||||
* gCodeVisualizer: https://github.com/hudbrog/gCodeViewer
|
||||
|
||||
The following software is recommended for Webcam support on the Raspberry Pi:
|
||||
|
||||
* MJPG-Streamer: http://sourceforge.net/apps/mediawiki/mjpg-streamer/index.php?title=Main_Page
|
||||
|
||||
I also want to thank [Janina Himmen](http://jhimmen.de/) for providing the kick-ass logo!
|
||||
|
||||
Why is it called OctoPrint and what's with the crystal ball in the logo?
|
||||
------------------------------------------------------------------------
|
||||
|
||||
It so happens that I needed a favicon and also OctoPrint's first name -- Printer WebUI -- simply lacked a certain coolness to it. So I asked The Internet(tm) for advise. After some brainstorming, the idea of a cute Octopus watching his print job remotely through a crystal ball was born... [or something like that](https://plus.google.com/u/0/106003970953341660077/posts/UmLD5mW8yBQ).
|
||||
|
|
|
@ -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(","))
|
|
@ -15,6 +15,8 @@ from octoprint.settings import settings
|
|||
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
SUPPORTED_EXTENSIONS=["gcode", "gco"]
|
||||
|
||||
class GcodeManager:
|
||||
def __init__(self):
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
@ -63,6 +65,8 @@ class GcodeManager:
|
|||
dirty = True
|
||||
if gcode.extrusionAmount:
|
||||
analysisResult["filament"] = "%.2fm" % (gcode.extrusionAmount / 1000)
|
||||
if gcode.calculateVolumeCm3():
|
||||
analysisResult["filament"] += " / %.2fcm³" % gcode.calculateVolumeCm3()
|
||||
dirty = True
|
||||
|
||||
if dirty:
|
||||
|
@ -114,28 +118,44 @@ class GcodeManager:
|
|||
#~~ file handling
|
||||
|
||||
def addFile(self, file):
|
||||
if file:
|
||||
absolutePath = self.getAbsolutePath(file.filename, mustExist=False)
|
||||
if absolutePath is not None:
|
||||
if file.filename in self._metadata.keys():
|
||||
# delete existing metadata entry, since the file is going to get overwritten
|
||||
del self._metadata[file.filename]
|
||||
self._metadataDirty = True
|
||||
self._saveMetadata()
|
||||
file.save(absolutePath)
|
||||
self._metadataAnalyzer.addFileToQueue(os.path.basename(absolutePath))
|
||||
return self._getBasicFilename(absolutePath)
|
||||
return None
|
||||
if not file:
|
||||
return None
|
||||
|
||||
absolutePath = self.getAbsolutePath(file.filename, mustExist=False)
|
||||
if absolutePath is None:
|
||||
return None
|
||||
|
||||
basename = self._getBasicFilename(absolutePath)
|
||||
if basename in self._metadata.keys():
|
||||
# delete existing metadata entry, since the file is going to get overwritten
|
||||
del self._metadata[basename]
|
||||
self._metadataDirty = True
|
||||
self._saveMetadata()
|
||||
file.save(absolutePath)
|
||||
self._metadataAnalyzer.addFileToQueue(basename)
|
||||
return basename
|
||||
|
||||
def getFutureFilename(self, file):
|
||||
if not file:
|
||||
return None
|
||||
|
||||
absolutePath = self.getAbsolutePath(file.filename, mustExist=False)
|
||||
if absolutePath is None:
|
||||
return None
|
||||
|
||||
return self._getBasicFilename(absolutePath)
|
||||
|
||||
def removeFile(self, filename):
|
||||
filename = self._getBasicFilename(filename)
|
||||
absolutePath = self.getAbsolutePath(filename)
|
||||
if absolutePath is not None:
|
||||
os.remove(absolutePath)
|
||||
if filename in self._metadata.keys():
|
||||
del self._metadata[filename]
|
||||
self._metadataDirty = True
|
||||
self._saveMetadata()
|
||||
if absolutePath is None:
|
||||
return
|
||||
|
||||
os.remove(absolutePath)
|
||||
if filename in self._metadata.keys():
|
||||
del self._metadata[filename]
|
||||
self._metadataDirty = True
|
||||
self._saveMetadata()
|
||||
|
||||
def getAbsolutePath(self, filename, mustExist=True):
|
||||
"""
|
||||
|
@ -153,7 +173,7 @@ class GcodeManager:
|
|||
"""
|
||||
filename = self._getBasicFilename(filename)
|
||||
|
||||
if not util.isAllowedFile(filename, set(["gcode"])):
|
||||
if not util.isAllowedFile(filename.lower(), set(SUPPORTED_EXTENSIONS)):
|
||||
return None
|
||||
|
||||
secure = os.path.join(self._uploadFolder, secure_filename(self._getBasicFilename(filename)))
|
||||
|
|
|
@ -8,10 +8,13 @@ import threading
|
|||
import copy
|
||||
import os
|
||||
|
||||
#import logging, logging.config
|
||||
|
||||
import octoprint.util.comm as comm
|
||||
import octoprint.util as util
|
||||
|
||||
from octoprint.settings import settings
|
||||
from octoprint.events import eventManager
|
||||
|
||||
def getConnectionOptions():
|
||||
"""
|
||||
|
@ -21,12 +24,16 @@ def getConnectionOptions():
|
|||
"ports": comm.serialList(),
|
||||
"baudrates": comm.baudrateList(),
|
||||
"portPreference": settings().get(["serial", "port"]),
|
||||
"baudratePreference": settings().getInt(["serial", "baudrate"])
|
||||
"baudratePreference": settings().getInt(["serial", "baudrate"]),
|
||||
"autoconnect": settings().getBoolean(["serial", "autoconnect"])
|
||||
}
|
||||
|
||||
class Printer():
|
||||
def __init__(self, gcodeManager):
|
||||
from collections import deque
|
||||
|
||||
self._gcodeManager = gcodeManager
|
||||
self._gcodeManager.registerCallback(self)
|
||||
|
||||
# state
|
||||
self._temp = None
|
||||
|
@ -34,19 +41,19 @@ class Printer():
|
|||
self._targetTemp = None
|
||||
self._targetBedTemp = None
|
||||
self._temps = {
|
||||
"actual": [],
|
||||
"target": [],
|
||||
"actualBed": [],
|
||||
"targetBed": []
|
||||
"actual": deque([], 300),
|
||||
"target": deque([], 300),
|
||||
"actualBed": deque([], 300),
|
||||
"targetBed": deque([], 300)
|
||||
}
|
||||
self._tempBacklog = []
|
||||
|
||||
self._latestMessage = None
|
||||
self._messages = []
|
||||
self._messages = deque([], 300)
|
||||
self._messageBacklog = []
|
||||
|
||||
self._latestLog = None
|
||||
self._log = []
|
||||
self._log = deque([], 300)
|
||||
self._logBacklog = []
|
||||
|
||||
self._state = None
|
||||
|
@ -57,16 +64,14 @@ class Printer():
|
|||
self._printTime = None
|
||||
self._printTimeLeft = None
|
||||
|
||||
# gcode handling
|
||||
self._gcodeList = None
|
||||
self._filename = None
|
||||
self._gcodeLoader = None
|
||||
self._printAfterSelect = False
|
||||
|
||||
# feedrate
|
||||
self._feedrateModifierMapping = {"outerWall": "WALL-OUTER", "innerWall": "WALL_INNER", "fill": "FILL", "support": "SUPPORT"}
|
||||
# sd handling
|
||||
self._sdPrinting = False
|
||||
self._sdStreaming = False
|
||||
self._sdFilelistAvailable = threading.Event()
|
||||
|
||||
# timelapse
|
||||
self._timelapse = None
|
||||
self._selectedFile = None
|
||||
|
||||
# comm
|
||||
self._comm = None
|
||||
|
@ -84,9 +89,8 @@ class Printer():
|
|||
)
|
||||
self._stateMonitor.reset(
|
||||
state={"state": None, "stateString": self.getStateString(), "flags": self._getStateFlags()},
|
||||
jobData={"filename": None, "lines": None, "estimatedPrintTime": None, "filament": None},
|
||||
gcodeData={"filename": None, "progress": None},
|
||||
progress={"progress": None, "printTime": None, "printTimeLeft": None},
|
||||
jobData={"filename": None, "filesize": None, "estimatedPrintTime": None, "filament": None},
|
||||
progress={"progress": None, "filepos": None, "printTime": None, "printTimeLeft": None},
|
||||
currentZ=None
|
||||
)
|
||||
|
||||
|
@ -120,7 +124,25 @@ class Printer():
|
|||
try: callback.sendCurrentData(copy.deepcopy(data))
|
||||
except: pass
|
||||
|
||||
#~~ printer commands
|
||||
def _sendTriggerUpdateCallbacks(self, type):
|
||||
for callback in self._callbacks:
|
||||
try: callback.sendUpdateTrigger(type)
|
||||
except: pass
|
||||
|
||||
def _sendFeedbackCommandOutput(self, name, output):
|
||||
for callback in self._callbacks:
|
||||
try: callback.sendFeedbackCommandOutput(name, output)
|
||||
except: pass
|
||||
|
||||
#~~ callback from gcodemanager
|
||||
|
||||
def sendUpdateTrigger(self, type):
|
||||
if type == "gcodeFiles" and self._selectedFile:
|
||||
self._setJobData(self._selectedFile["filename"],
|
||||
self._selectedFile["filesize"],
|
||||
self._selectedFile["sd"])
|
||||
|
||||
#~~ printer commands
|
||||
|
||||
def connect(self, port=None, baudrate=None):
|
||||
"""
|
||||
|
@ -138,6 +160,7 @@ class Printer():
|
|||
if self._comm is not None:
|
||||
self._comm.close()
|
||||
self._comm = None
|
||||
eventManager().fire("Disconnected")
|
||||
|
||||
def command(self, command):
|
||||
"""
|
||||
|
@ -149,48 +172,48 @@ class Printer():
|
|||
"""
|
||||
Sends multiple gcode commands (provided as a list) to the printer.
|
||||
"""
|
||||
if self._comm is None:
|
||||
return
|
||||
|
||||
for command in commands:
|
||||
self._comm.sendCommand(command)
|
||||
|
||||
def setFeedrateModifier(self, structure, percentage):
|
||||
if (not self._feedrateModifierMapping.has_key(structure)) or percentage < 0:
|
||||
def setTemperatureOffset(self, extruder, bed):
|
||||
if self._comm is None:
|
||||
return
|
||||
|
||||
self._comm.setFeedrateModifier(self._feedrateModifierMapping[structure], percentage / 100.0)
|
||||
self._comm.setTemperatureOffset(extruder, bed)
|
||||
self._stateMonitor.setTempOffsets(extruder, bed)
|
||||
|
||||
def loadGcode(self, file, printAfterLoading=False):
|
||||
"""
|
||||
Loads the gcode from the given file as the new print job.
|
||||
Aborts if the printer is currently printing or another gcode file is currently being loaded.
|
||||
"""
|
||||
if (self._comm is not None and self._comm.isPrinting()) or (self._gcodeLoader is not None):
|
||||
def selectFile(self, filename, sd, printAfterSelect=False):
|
||||
if self._comm is None or (self._comm.isBusy() or self._comm.isStreaming()):
|
||||
return
|
||||
|
||||
self._setJobData(None, None)
|
||||
self._printAfterSelect = printAfterSelect
|
||||
self._comm.selectFile(filename, sd)
|
||||
self._setProgressData(0, None, None, None)
|
||||
self._setCurrentZ(None)
|
||||
|
||||
onGcodeLoadedCallback = self._onGcodeLoaded
|
||||
if printAfterLoading:
|
||||
onGcodeLoadedCallback = self._onGcodeLoadedToPrint
|
||||
def unselectFile(self):
|
||||
if self._comm is not None and (self._comm.isBusy() or self._comm.isStreaming()):
|
||||
return
|
||||
|
||||
self._gcodeLoader = GcodeLoader(file, self._onGcodeLoadingProgress, onGcodeLoadedCallback)
|
||||
self._gcodeLoader.start()
|
||||
self._comm.unselectFile()
|
||||
self._setProgressData(0, None, None, None)
|
||||
self._setCurrentZ(None)
|
||||
|
||||
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
|
||||
|
||||
def startPrint(self):
|
||||
"""
|
||||
Starts the currently loaded print job.
|
||||
Only starts if the printer is connected and operational, not currently printing and a printjob is loaded
|
||||
"""
|
||||
if self._comm is None or not self._comm.isOperational():
|
||||
if self._comm is None or not self._comm.isOperational() or self._comm.isPrinting():
|
||||
return
|
||||
if self._gcodeList is None:
|
||||
return
|
||||
if self._comm.isPrinting():
|
||||
if self._selectedFile is None:
|
||||
return
|
||||
|
||||
self._setCurrentZ(-1)
|
||||
self._comm.printGCode(self._gcodeList)
|
||||
self._setCurrentZ(None)
|
||||
self._comm.startPrint()
|
||||
|
||||
def togglePausePrint(self):
|
||||
"""
|
||||
|
@ -198,6 +221,7 @@ class Printer():
|
|||
"""
|
||||
if self._comm is None:
|
||||
return
|
||||
|
||||
self._comm.setPause(not self._comm.isPaused())
|
||||
|
||||
def cancelPrint(self, disableMotorsAndHeater=True):
|
||||
|
@ -206,28 +230,23 @@ class Printer():
|
|||
"""
|
||||
if self._comm is None:
|
||||
return
|
||||
|
||||
self._comm.cancelPrint()
|
||||
|
||||
if disableMotorsAndHeater:
|
||||
self.commands(["M84", "M104 S0", "M140 S0", "M106 S0"]) # disable motors, switch off heaters and fan
|
||||
|
||||
# reset line, height, print time
|
||||
# reset progress, height, print time
|
||||
self._setCurrentZ(None)
|
||||
self._setProgressData(None, None, None)
|
||||
self._setProgressData(None, None, None, None)
|
||||
|
||||
# mark print as failure
|
||||
self._gcodeManager.printFailed(self._filename)
|
||||
if self._selectedFile is not None:
|
||||
self._gcodeManager.printFailed(self._selectedFile["filename"])
|
||||
eventManager().fire("PrintFailed", self._selectedFile["filename"])
|
||||
|
||||
#~~ state monitoring
|
||||
|
||||
def setTimelapse(self, timelapse):
|
||||
if self._timelapse is not None and self.isPrinting():
|
||||
self._timelapse.onPrintjobStopped()
|
||||
del self._timelapse
|
||||
self._timelapse = timelapse
|
||||
|
||||
def getTimelapse(self):
|
||||
return self._timelapse
|
||||
|
||||
def _setCurrentZ(self, currentZ):
|
||||
self._currentZ = currentZ
|
||||
|
||||
|
@ -242,15 +261,13 @@ class Printer():
|
|||
|
||||
def _addLog(self, log):
|
||||
self._log.append(log)
|
||||
self._log = self._log[-300:]
|
||||
self._stateMonitor.addLog(log)
|
||||
|
||||
def _addMessage(self, message):
|
||||
self._messages.append(message)
|
||||
self._messages = self._messages[-300:]
|
||||
self._stateMonitor.addMessage(message)
|
||||
|
||||
def _setProgressData(self, progress, printTime, printTimeLeft):
|
||||
def _setProgressData(self, progress, filepos, printTime, printTimeLeft):
|
||||
self._progress = progress
|
||||
self._printTime = printTime
|
||||
self._printTimeLeft = printTimeLeft
|
||||
|
@ -263,22 +280,19 @@ class Printer():
|
|||
if (self._printTimeLeft):
|
||||
formattedPrintTimeLeft = util.getFormattedTimeDelta(datetime.timedelta(minutes=self._printTimeLeft))
|
||||
|
||||
self._stateMonitor.setProgress({"progress": self._progress, "printTime": formattedPrintTime, "printTimeLeft": formattedPrintTimeLeft})
|
||||
formattedFilePos = None
|
||||
if (filepos):
|
||||
formattedFilePos = util.getFormattedSize(filepos)
|
||||
|
||||
self._stateMonitor.setProgress({"progress": self._progress, "filepos": formattedFilePos, "printTime": formattedPrintTime, "printTimeLeft": formattedPrintTimeLeft})
|
||||
|
||||
def _addTemperatureData(self, temp, bedTemp, targetTemp, bedTargetTemp):
|
||||
currentTimeUtc = int(time.time() * 1000)
|
||||
|
||||
self._temps["actual"].append((currentTimeUtc, temp))
|
||||
self._temps["actual"] = self._temps["actual"][-300:]
|
||||
|
||||
self._temps["target"].append((currentTimeUtc, targetTemp))
|
||||
self._temps["target"] = self._temps["target"][-300:]
|
||||
|
||||
self._temps["actualBed"].append((currentTimeUtc, bedTemp))
|
||||
self._temps["actualBed"] = self._temps["actualBed"][-300:]
|
||||
|
||||
self._temps["targetBed"].append((currentTimeUtc, bedTargetTemp))
|
||||
self._temps["targetBed"] = self._temps["targetBed"][-300:]
|
||||
|
||||
self._temp = temp
|
||||
self._bedTemp = bedTemp
|
||||
|
@ -287,19 +301,31 @@ class Printer():
|
|||
|
||||
self._stateMonitor.addTemperature({"currentTime": currentTimeUtc, "temp": self._temp, "bedTemp": self._bedTemp, "targetTemp": self._targetTemp, "targetBedTemp": self._targetBedTemp})
|
||||
|
||||
def _setJobData(self, filename, gcodeList):
|
||||
self._filename = filename
|
||||
self._gcodeList = gcodeList
|
||||
|
||||
lines = None
|
||||
if self._gcodeList:
|
||||
lines = len(self._gcodeList)
|
||||
def _setJobData(self, filename, filesize, sd):
|
||||
if filename is not None:
|
||||
self._selectedFile = {
|
||||
"filename": filename,
|
||||
"filesize": filesize,
|
||||
"sd": sd
|
||||
}
|
||||
else:
|
||||
self._selectedFile = None
|
||||
|
||||
formattedFilename = None
|
||||
formattedFilesize = None
|
||||
estimatedPrintTime = None
|
||||
fileMTime = None
|
||||
filament = None
|
||||
if self._filename:
|
||||
formattedFilename = os.path.basename(self._filename)
|
||||
if filename:
|
||||
formattedFilename = os.path.basename(filename)
|
||||
|
||||
# Use a string for mtime because it could be float and the
|
||||
# javascript needs to exact match
|
||||
if not sd:
|
||||
fileMTime = str(os.stat(filename).st_mtime)
|
||||
|
||||
if filesize:
|
||||
formattedFilesize = util.getFormattedSize(filesize)
|
||||
|
||||
fileData = self._gcodeManager.getFileData(filename)
|
||||
if fileData is not None and "gcodeAnalysis" in fileData.keys():
|
||||
|
@ -308,15 +334,17 @@ class Printer():
|
|||
if "filament" in fileData["gcodeAnalysis"].keys():
|
||||
filament = fileData["gcodeAnalysis"]["filament"]
|
||||
|
||||
self._stateMonitor.setJobData({"filename": formattedFilename, "lines": lines, "estimatedPrintTime": estimatedPrintTime, "filament": filament})
|
||||
self._stateMonitor.setJobData({"filename": formattedFilename, "filesize": formattedFilesize, "estimatedPrintTime": estimatedPrintTime, "filament": filament, "sd": sd, "mtime": fileMTime})
|
||||
|
||||
def _sendInitialStateUpdate(self, callback):
|
||||
try:
|
||||
data = self._stateMonitor.getCurrentData()
|
||||
# convert the dict of deques to a dict of lists
|
||||
temps = {k: list(v) for (k,v) in self._temps.iteritems()}
|
||||
data.update({
|
||||
"temperatureHistory": self._temps,
|
||||
"logHistory": self._log,
|
||||
"messageHistory": self._messages
|
||||
"temperatureHistory": temps,
|
||||
"logHistory": list(self._log),
|
||||
"messageHistory": list(self._messages)
|
||||
})
|
||||
callback.sendHistoryData(data)
|
||||
except Exception, err:
|
||||
|
@ -325,16 +353,24 @@ class Printer():
|
|||
pass
|
||||
|
||||
def _getStateFlags(self):
|
||||
if not settings().getBoolean(["feature", "sdSupport"]) or self._comm is None:
|
||||
sdReady = False
|
||||
else:
|
||||
sdReady = self._comm.isSdReady()
|
||||
|
||||
return {
|
||||
"operational": self.isOperational(),
|
||||
"printing": self.isPrinting(),
|
||||
"closedOrError": self.isClosedOrError(),
|
||||
"error": self.isError(),
|
||||
"loading": self.isLoading(),
|
||||
"paused": self.isPaused(),
|
||||
"ready": self.isReady()
|
||||
"ready": self.isReady(),
|
||||
"sdReady": sdReady
|
||||
}
|
||||
|
||||
def getCurrentData(self):
|
||||
return self._stateMonitor.getCurrentData()
|
||||
|
||||
#~~ callbacks triggered from self._comm
|
||||
|
||||
def mcLog(self, message):
|
||||
|
@ -352,26 +388,19 @@ class Printer():
|
|||
"""
|
||||
oldState = self._state
|
||||
|
||||
# forward relevant state changes to timelapse
|
||||
if self._timelapse is not None:
|
||||
if oldState == self._comm.STATE_PRINTING and state != self._comm.STATE_PAUSED:
|
||||
self._timelapse.onPrintjobStopped()
|
||||
elif state == self._comm.STATE_PRINTING and oldState != self._comm.STATE_PAUSED:
|
||||
self._timelapse.onPrintjobStarted(self._filename)
|
||||
|
||||
# forward relevant state changes to gcode manager
|
||||
if self._comm is not None and oldState == self._comm.STATE_PRINTING:
|
||||
if state == self._comm.STATE_OPERATIONAL:
|
||||
self._gcodeManager.printSucceeded(self._filename)
|
||||
elif state == self._comm.STATE_CLOSED or state == self._comm.STATE_ERROR or state == self._comm.STATE_CLOSED_WITH_ERROR:
|
||||
self._gcodeManager.printFailed(self._filename)
|
||||
self._gcodeManager.resumeAnalysis() # do not analyse gcode while printing
|
||||
if self._selectedFile is not None:
|
||||
if state == self._comm.STATE_OPERATIONAL:
|
||||
self._gcodeManager.printSucceeded(self._selectedFile["filename"])
|
||||
elif state == self._comm.STATE_CLOSED or state == self._comm.STATE_ERROR or state == self._comm.STATE_CLOSED_WITH_ERROR:
|
||||
self._gcodeManager.printFailed(self._selectedFile["filename"])
|
||||
self._gcodeManager.resumeAnalysis() # printing done, put those cpu cycles to good use
|
||||
elif self._comm is not None and state == self._comm.STATE_PRINTING:
|
||||
self._gcodeManager.pauseAnalysis() # printing done, put those cpu cycles to good use
|
||||
self._gcodeManager.pauseAnalysis() # do not analyse gcode while printing
|
||||
|
||||
self._setState(state)
|
||||
|
||||
|
||||
def mcMessage(self, message):
|
||||
"""
|
||||
Callback method for the comm object, called upon message exchanges via serial.
|
||||
|
@ -379,66 +408,109 @@ class Printer():
|
|||
"""
|
||||
self._addMessage(message)
|
||||
|
||||
def mcProgress(self, lineNr):
|
||||
def mcProgress(self):
|
||||
"""
|
||||
Callback method for the comm object, called upon any change in progress of the printjob.
|
||||
Triggers storage of new values for printTime, printTimeLeft and the current line.
|
||||
Triggers storage of new values for printTime, printTimeLeft and the current progress.
|
||||
"""
|
||||
oldProgress = self._progress
|
||||
|
||||
if self._timelapse is not None:
|
||||
try: self._timelapse.onPrintjobProgress(oldProgress, self._progress, int(round(self._progress * 100 / len(self._gcodeList))))
|
||||
except: pass
|
||||
|
||||
self._setProgressData(self._comm.getPrintPos(), self._comm.getPrintTime(), self._comm.getPrintTimeRemainingEstimate())
|
||||
self._setProgressData(self._comm.getPrintProgress(), self._comm.getPrintFilepos(), self._comm.getPrintTime(), self._comm.getPrintTimeRemainingEstimate())
|
||||
|
||||
def mcZChange(self, newZ):
|
||||
"""
|
||||
Callback method for the comm object, called upon change of the z-layer.
|
||||
"""
|
||||
oldZ = self._currentZ
|
||||
if self._timelapse is not None:
|
||||
self._timelapse.onZChange(oldZ, newZ)
|
||||
if newZ != oldZ:
|
||||
# we have to react to all z-changes, even those that might "go backward" due to a slicer's retraction or
|
||||
# anti-backlash-routines. Event subscribes should individually take care to filter out "wrong" z-changes
|
||||
eventManager().fire("ZChange", newZ)
|
||||
|
||||
self._setCurrentZ(newZ)
|
||||
|
||||
#~~ callbacks triggered by gcodeLoader
|
||||
|
||||
def _onGcodeLoadingProgress(self, filename, progress, mode):
|
||||
formattedFilename = None
|
||||
if filename is not None:
|
||||
formattedFilename = os.path.basename(filename)
|
||||
|
||||
self._stateMonitor.setGcodeData({"filename": formattedFilename, "progress": progress, "mode": mode})
|
||||
|
||||
def _onGcodeLoaded(self, filename, gcodeList):
|
||||
self._setJobData(filename, gcodeList)
|
||||
self._setCurrentZ(None)
|
||||
self._setProgressData(None, None, None)
|
||||
self._gcodeLoader = None
|
||||
|
||||
self._stateMonitor.setGcodeData({"filename": None, "progress": None})
|
||||
def mcSdStateChange(self, sdReady):
|
||||
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
|
||||
|
||||
def _onGcodeLoadedToPrint(self, filename, gcodeList):
|
||||
self._onGcodeLoaded(filename, gcodeList)
|
||||
self.startPrint()
|
||||
def mcSdFiles(self, files):
|
||||
self._sendTriggerUpdateCallbacks("gcodeFiles")
|
||||
self._sdFilelistAvailable.set()
|
||||
|
||||
def mcFileSelected(self, filename, filesize, sd):
|
||||
self._setJobData(filename, filesize, sd)
|
||||
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
|
||||
|
||||
if self._printAfterSelect:
|
||||
self.startPrint()
|
||||
|
||||
def mcPrintjobDone(self):
|
||||
self._setProgressData(1.0, self._selectedFile["filesize"], self._comm.getPrintTime(), 0)
|
||||
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
|
||||
|
||||
def mcFileTransferStarted(self, filename, filesize):
|
||||
self._sdStreaming = True
|
||||
|
||||
self._setJobData(filename, filesize, True)
|
||||
self._setProgressData(0.0, 0, 0, None)
|
||||
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
|
||||
|
||||
def mcFileTransferDone(self):
|
||||
self._sdStreaming = False
|
||||
|
||||
self._setCurrentZ(None)
|
||||
self._setJobData(None, None, None)
|
||||
self._setProgressData(None, None, None, None)
|
||||
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
|
||||
|
||||
def mcReceivedRegisteredMessage(self, command, output):
|
||||
self._sendFeedbackCommandOutput(command, output)
|
||||
|
||||
#~~ sd file handling
|
||||
|
||||
def getSdFiles(self):
|
||||
if self._comm is None or not self._comm.isSdReady():
|
||||
return []
|
||||
return self._comm.getSdFiles()
|
||||
|
||||
def addSdFile(self, filename, path):
|
||||
if not self._comm or self._comm.isBusy() or not self._comm.isSdReady():
|
||||
return
|
||||
|
||||
self.refreshSdFiles(blocking=True)
|
||||
existingSdFiles = self._comm.getSdFiles()
|
||||
|
||||
sdFilename = util.getDosFilename(filename, existingSdFiles)
|
||||
self._comm.startFileTransfer(path, sdFilename)
|
||||
|
||||
def deleteSdFile(self, filename):
|
||||
if not self._comm or not self._comm.isSdReady():
|
||||
return
|
||||
self._comm.deleteSdFile(filename)
|
||||
|
||||
def initSdCard(self):
|
||||
if not self._comm or self._comm.isSdReady():
|
||||
return
|
||||
self._comm.initSdCard()
|
||||
|
||||
def releaseSdCard(self):
|
||||
if not self._comm or not self._comm.isSdReady():
|
||||
return
|
||||
self._comm.releaseSdCard()
|
||||
|
||||
def refreshSdFiles(self, blocking=False):
|
||||
"""
|
||||
Refreshs the list of file stored on the SD card attached to printer (if available and printer communication
|
||||
available). Optional blocking parameter allows making the method block (max 10s) until the file list has been
|
||||
received (and can be accessed via self._comm.getSdFiles()). Defaults to a asynchronous operation.
|
||||
"""
|
||||
if not self._comm or not self._comm.isSdReady():
|
||||
return
|
||||
self._sdFilelistAvailable.clear()
|
||||
self._comm.refreshSdFiles()
|
||||
if blocking:
|
||||
self._sdFilelistAvailable.wait(10000)
|
||||
|
||||
#~~ state reports
|
||||
|
||||
def feedrateState(self):
|
||||
if self._comm is not None:
|
||||
feedrateModifiers = self._comm.getFeedrateModifiers()
|
||||
result = {}
|
||||
for structure in self._feedrateModifierMapping.keys():
|
||||
if (feedrateModifiers.has_key(self._feedrateModifierMapping[structure])):
|
||||
result[structure] = int(round(feedrateModifiers[self._feedrateModifierMapping[structure]] * 100))
|
||||
else:
|
||||
result[structure] = 100
|
||||
return result
|
||||
else:
|
||||
return None
|
||||
|
||||
def getStateString(self):
|
||||
"""
|
||||
Returns a human readable string corresponding to the current communication state.
|
||||
|
@ -448,6 +520,33 @@ class Printer():
|
|||
else:
|
||||
return self._comm.getStateString()
|
||||
|
||||
def getCurrentData(self):
|
||||
return self._stateMonitor.getCurrentData()
|
||||
|
||||
def getCurrentJob(self):
|
||||
currentData = self._stateMonitor.getCurrentData()
|
||||
return currentData["job"]
|
||||
|
||||
def getCurrentTemperatures(self):
|
||||
if self._comm is not None:
|
||||
(tempOffset, bedTempOffset) = self._comm.getOffsets()
|
||||
else:
|
||||
tempOffset = 0
|
||||
bedTempOffset = 0
|
||||
|
||||
return {
|
||||
"extruder": {
|
||||
"current": self._temp,
|
||||
"target": self._targetTemp,
|
||||
"offset": tempOffset
|
||||
},
|
||||
"bed": {
|
||||
"current": self._bedTemp,
|
||||
"target": self._targetBedTemp,
|
||||
"offset": bedTempOffset
|
||||
}
|
||||
}
|
||||
|
||||
def isClosedOrError(self):
|
||||
return self._comm is None or self._comm.isClosedOrError()
|
||||
|
||||
|
@ -464,7 +563,7 @@ class Printer():
|
|||
return self._comm is not None and self._comm.isError()
|
||||
|
||||
def isReady(self):
|
||||
return self._gcodeLoader is None and self._gcodeList and len(self._gcodeList) > 0
|
||||
return self.isOperational() and not self._comm.isStreaming()
|
||||
|
||||
def isLoading(self):
|
||||
return self._gcodeLoader is not None
|
||||
|
@ -488,7 +587,7 @@ class GcodeLoader(threading.Thread):
|
|||
def run(self):
|
||||
#Send an initial M110 to reset the line counter to zero.
|
||||
prevLineType = lineType = "CUSTOM"
|
||||
gcodeList = ["M110"]
|
||||
gcodeList = ["M110 N0"]
|
||||
filesize = os.stat(self._filename).st_size
|
||||
with open(self._filename, "r") as file:
|
||||
for line in file:
|
||||
|
@ -514,6 +613,38 @@ class GcodeLoader(threading.Thread):
|
|||
def _onParsingProgress(self, progress):
|
||||
self._progressCallback(self._filename, progress, "parsing")
|
||||
|
||||
class SdFileStreamer(threading.Thread):
|
||||
def __init__(self, comm, filename, file, progressCallback, finishCallback):
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
self._comm = comm
|
||||
self._filename = filename
|
||||
self._file = file
|
||||
self._progressCallback = progressCallback
|
||||
self._finishCallback = finishCallback
|
||||
|
||||
def run(self):
|
||||
if self._comm.isBusy():
|
||||
return
|
||||
|
||||
name = self._filename[:self._filename.rfind(".")]
|
||||
sdFilename = name[:8].lower() + ".gco"
|
||||
try:
|
||||
size = os.stat(self._file).st_size
|
||||
with open(self._file, "r") as f:
|
||||
self._comm.startSdFileTransfer(sdFilename)
|
||||
for line in f:
|
||||
if ";" in line:
|
||||
line = line[0:line.find(";")]
|
||||
line = line.strip()
|
||||
if len(line) > 0:
|
||||
self._comm.sendCommand(line)
|
||||
time.sleep(0.001) # do not send too fast
|
||||
self._progressCallback(sdFilename, float(f.tell()) / float(size))
|
||||
finally:
|
||||
self._comm.endSdFileTransfer(sdFilename)
|
||||
self._finishCallback(sdFilename)
|
||||
|
||||
class StateMonitor(object):
|
||||
def __init__(self, ratelimit, updateCallback, addTemperatureCallback, addLogCallback, addMessageCallback):
|
||||
self._ratelimit = ratelimit
|
||||
|
@ -525,9 +656,13 @@ class StateMonitor(object):
|
|||
self._state = None
|
||||
self._jobData = None
|
||||
self._gcodeData = None
|
||||
self._sdUploadData = None
|
||||
self._currentZ = None
|
||||
self._progress = None
|
||||
|
||||
self._tempOffset = 0
|
||||
self._bedTempOffset = 0
|
||||
|
||||
self._changeEvent = threading.Event()
|
||||
|
||||
self._lastUpdate = time.time()
|
||||
|
@ -535,10 +670,9 @@ class StateMonitor(object):
|
|||
self._worker.daemon = True
|
||||
self._worker.start()
|
||||
|
||||
def reset(self, state=None, jobData=None, gcodeData=None, progress=None, currentZ=None):
|
||||
def reset(self, state=None, jobData=None, progress=None, currentZ=None):
|
||||
self.setState(state)
|
||||
self.setJobData(jobData)
|
||||
self.setGcodeData(gcodeData)
|
||||
self.setProgress(progress)
|
||||
self.setCurrentZ(currentZ)
|
||||
|
||||
|
@ -566,14 +700,17 @@ class StateMonitor(object):
|
|||
self._jobData = jobData
|
||||
self._changeEvent.set()
|
||||
|
||||
def setGcodeData(self, gcodeData):
|
||||
self._gcodeData = gcodeData
|
||||
self._changeEvent.set()
|
||||
|
||||
def setProgress(self, progress):
|
||||
self._progress = progress
|
||||
self._changeEvent.set()
|
||||
|
||||
def setTempOffsets(self, tempOffset, bedTempOffset):
|
||||
if tempOffset is not None:
|
||||
self._tempOffset = tempOffset
|
||||
if bedTempOffset is not None:
|
||||
self._bedTempOffset = bedTempOffset
|
||||
self._changeEvent.set()
|
||||
|
||||
def _work(self):
|
||||
while True:
|
||||
self._changeEvent.wait()
|
||||
|
@ -593,8 +730,8 @@ class StateMonitor(object):
|
|||
return {
|
||||
"state": self._state,
|
||||
"job": self._jobData,
|
||||
"gcode": self._gcodeData,
|
||||
"currentZ": self._currentZ,
|
||||
"progress": self._progress
|
||||
"progress": self._progress,
|
||||
"offsets": (self._tempOffset, self._bedTempOffset)
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -2,11 +2,12 @@
|
|||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
|
||||
import ConfigParser
|
||||
import sys
|
||||
import os
|
||||
import yaml
|
||||
import logging
|
||||
import re
|
||||
import uuid
|
||||
|
||||
APPNAME="OctoPrint"
|
||||
|
||||
|
@ -24,28 +25,48 @@ def settings(init=False, configfile=None, basedir=None):
|
|||
default_settings = {
|
||||
"serial": {
|
||||
"port": None,
|
||||
"baudrate": None
|
||||
"baudrate": None,
|
||||
"autoconnect": False,
|
||||
"log": False,
|
||||
"timeout": {
|
||||
"detection": 0.5,
|
||||
"connection": 2,
|
||||
"communication": 5
|
||||
},
|
||||
"additionalPorts": []
|
||||
},
|
||||
"server": {
|
||||
"host": "0.0.0.0",
|
||||
"port": 5000
|
||||
"port": 5000,
|
||||
"firstRun": True
|
||||
},
|
||||
"webcam": {
|
||||
"stream": None,
|
||||
"snapshot": None,
|
||||
"ffmpeg": None,
|
||||
"bitrate": "5000k",
|
||||
"watermark": True
|
||||
"watermark": True,
|
||||
"flipH": False,
|
||||
"flipV": False,
|
||||
"timelapse": {
|
||||
"type": "off",
|
||||
"options": {}
|
||||
}
|
||||
},
|
||||
"feature": {
|
||||
"gCodeVisualizer": True,
|
||||
"waitForStartOnConnect": False
|
||||
"temperatureGraph": True,
|
||||
"waitForStartOnConnect": False,
|
||||
"alwaysSendChecksum": False,
|
||||
"sdSupport": True,
|
||||
"swallowOkAfterResend": False
|
||||
},
|
||||
"folder": {
|
||||
"uploads": None,
|
||||
"timelapse": None,
|
||||
"timelapse_tmp": None,
|
||||
"logs": None
|
||||
"logs": None,
|
||||
"virtualSd": None
|
||||
},
|
||||
"temperature": {
|
||||
"profiles":
|
||||
|
@ -60,7 +81,8 @@ default_settings = {
|
|||
"y": 6000,
|
||||
"z": 200,
|
||||
"e": 300
|
||||
}
|
||||
},
|
||||
"pauseTriggers": []
|
||||
},
|
||||
"appearance": {
|
||||
"name": "",
|
||||
|
@ -71,9 +93,36 @@ default_settings = {
|
|||
"actions": []
|
||||
},
|
||||
"accessControl": {
|
||||
"enabled": False,
|
||||
"enabled": True,
|
||||
"userManager": "octoprint.users.FilebasedUserManager",
|
||||
"userfile": None
|
||||
"userfile": None,
|
||||
"autologinLocal": False,
|
||||
"localNetworks": ["127.0.0.0/8"],
|
||||
"autologinAs": None
|
||||
},
|
||||
"events": {
|
||||
"systemCommandTrigger": {
|
||||
"enabled": False
|
||||
},
|
||||
"gcodeCommandTrigger": {
|
||||
"enabled": False
|
||||
}
|
||||
},
|
||||
"api": {
|
||||
"enabled": False,
|
||||
"key": ''.join('%02X' % ord(z) for z in uuid.uuid4().bytes)
|
||||
},
|
||||
"terminalFilters": [
|
||||
{ "name": "Suppress M105 requests/responses", "regex": "(Send: M105)|(Recv: ok T:)" },
|
||||
{ "name": "Suppress M27 requests/responses", "regex": "(Send: M27)|(Recv: SD printing byte)" }
|
||||
],
|
||||
"devel": {
|
||||
"virtualPrinter": {
|
||||
"enabled": False,
|
||||
"okAfterResend": False,
|
||||
"forceChecksum": False,
|
||||
"okWithLinenumber": False
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -115,7 +164,8 @@ class Settings(object):
|
|||
if os.path.exists(self._configfile) and os.path.isfile(self._configfile):
|
||||
with open(self._configfile, "r") as f:
|
||||
self._config = yaml.safe_load(f)
|
||||
else:
|
||||
# chamged from else to handle cases where the file exists, but is empty / 0 bytes
|
||||
if not self._config:
|
||||
self._config = {}
|
||||
|
||||
def save(self, force=False):
|
||||
|
@ -178,6 +228,17 @@ class Settings(object):
|
|||
self._logger.warn("Could not convert %r to a valid integer when getting option %r" % (value, path))
|
||||
return None
|
||||
|
||||
def getFloat(self, path):
|
||||
value = self.get(path)
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
self._logger.warn("Could not convert %r to a valid integer when getting option %r" % (value, path))
|
||||
return None
|
||||
|
||||
def getBoolean(self, path):
|
||||
value = self.get(path)
|
||||
if value is None:
|
||||
|
@ -199,6 +260,54 @@ class Settings(object):
|
|||
|
||||
return folder
|
||||
|
||||
def getFeedbackControls(self):
|
||||
feedbackControls = []
|
||||
for control in self.get(["controls"]):
|
||||
feedbackControls.extend(self._getFeedbackControls(control))
|
||||
return feedbackControls
|
||||
|
||||
def _getFeedbackControls(self, control=None):
|
||||
if control["type"] == "feedback_command" or control["type"] == "feedback":
|
||||
pattern = control["regex"]
|
||||
try:
|
||||
matcher = re.compile(pattern)
|
||||
return [(control["name"], matcher, control["template"])]
|
||||
except:
|
||||
# invalid regex or something like this, we'll just skip this entry
|
||||
pass
|
||||
elif control["type"] == "section":
|
||||
result = []
|
||||
for c in control["children"]:
|
||||
result.extend(self._getFeedbackControls(c))
|
||||
return result
|
||||
else:
|
||||
return []
|
||||
|
||||
def getPauseTriggers(self):
|
||||
triggers = {
|
||||
"enable": [],
|
||||
"disable": [],
|
||||
"toggle": []
|
||||
}
|
||||
for trigger in self.get(["printerParameters", "pauseTriggers"]):
|
||||
try:
|
||||
regex = trigger["regex"]
|
||||
type = trigger["type"]
|
||||
if type in triggers.keys():
|
||||
# make sure regex is valid
|
||||
re.compile(regex)
|
||||
# add to type list
|
||||
triggers[type].append(regex)
|
||||
except:
|
||||
# invalid regex or something like this, we'll just skip this entry
|
||||
pass
|
||||
|
||||
result = {}
|
||||
for type in triggers.keys():
|
||||
if len(triggers[type]) > 0:
|
||||
result[type] = re.compile("|".join(map(lambda x: "(%s)" % x, triggers[type])))
|
||||
return result
|
||||
|
||||
#~~ setter
|
||||
|
||||
def set(self, path, value, force=False):
|
||||
|
@ -234,6 +343,7 @@ class Settings(object):
|
|||
def setInt(self, path, value, force=False):
|
||||
if value is None:
|
||||
self.set(path, None, force)
|
||||
return
|
||||
|
||||
try:
|
||||
intValue = int(value)
|
||||
|
@ -243,6 +353,19 @@ class Settings(object):
|
|||
|
||||
self.set(path, intValue, force)
|
||||
|
||||
def setFloat(self, path, value, force=False):
|
||||
if value is None:
|
||||
self.set(path, None, force)
|
||||
return
|
||||
|
||||
try:
|
||||
floatValue = float(value)
|
||||
except ValueError:
|
||||
self._logger.warn("Could not convert %r to a valid integer when setting option %r" % (value, path))
|
||||
return
|
||||
|
||||
self.set(path, floatValue, force)
|
||||
|
||||
def setBoolean(self, path, value, force=False):
|
||||
if value is None or isinstance(value, bool):
|
||||
self.set(path, value, force)
|
||||
|
|
|
@ -5,13 +5,48 @@ body {
|
|||
}
|
||||
|
||||
.navbar-inner-text (@base) {
|
||||
text-shadow: 0 1px 0 lighten(@base, 15%);
|
||||
color: contrast(@base, #333333, #f2f2f2);
|
||||
text-shadow: 0 1px 0 contrast(@base, lighten(@base, 15%), darken(@base, 15%));
|
||||
color: @text-color;
|
||||
@caret-color: average(@base, @text-color);
|
||||
@caret-hover-color: average(@caret-color, @text-color);
|
||||
|
||||
.caret {
|
||||
border-bottom-color: @caret-color;
|
||||
border-top-color: @caret-color;
|
||||
}
|
||||
|
||||
&:hover .caret, &:focus .caret {
|
||||
border-bottom-color: @caret-hover-color;
|
||||
border-top-color: @caret-hover-color;
|
||||
}
|
||||
}
|
||||
|
||||
.brand (@color, @dark, @light) when (@color = @dark) {
|
||||
span {
|
||||
background-image: url(../img/tentacle-20x20.png);
|
||||
|
||||
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
|
||||
background-image: url(../img/tentacle-20x20@2x.png);
|
||||
}
|
||||
}
|
||||
}
|
||||
.brand (@color, @dark, @light) when (@color = @light) {
|
||||
span {
|
||||
background-image: url(../img/tentacle-20x20-light.png);
|
||||
|
||||
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
|
||||
background-image: url(../img/tentacle-20x20-light@2x.png);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-inner-color (@base) {
|
||||
@top: lighten(@base, 25%);
|
||||
@bottom: darken(@base, 15%);
|
||||
|
||||
@text-color-light: #f2f2f2;
|
||||
@text-color-dark: #333333;
|
||||
@text-color: contrast(@base, @text-color-dark, @text-color-light);
|
||||
|
||||
background-color: @base; /* fallback color if gradients are not supported */
|
||||
background-image: -webkit-linear-gradient(top, @top, @bottom); /* For Chrome and Safari */
|
||||
|
@ -23,6 +58,10 @@ body {
|
|||
.brand, .nav>li>a {
|
||||
.navbar-inner-text(@base);
|
||||
}
|
||||
|
||||
.brand {
|
||||
.brand(@text-color, @text-color-dark, @text-color-light);
|
||||
}
|
||||
|
||||
.nav {
|
||||
li.dropdown.open>.dropdown-toggle, li.dropdown.active>.dropdown-toggle, li.dropdown.open.active>.dropdown-toggle {
|
||||
|
@ -65,9 +104,16 @@ body {
|
|||
@base: #7728FF;
|
||||
.navbar-inner-color(@base);
|
||||
}
|
||||
|
||||
.brand img {
|
||||
vertical-align: bottom;
|
||||
&.black {
|
||||
@base: #383838;
|
||||
.navbar-inner-color(@base);
|
||||
}
|
||||
|
||||
.brand span {
|
||||
background-size: 20px 20px;
|
||||
background-position: left center;
|
||||
padding-left: 24px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -111,10 +157,10 @@ body {
|
|||
|
||||
.octoprint-container {
|
||||
.accordion-heading {
|
||||
.settings-trigger {
|
||||
.accordion-heading-button {
|
||||
float: right;
|
||||
|
||||
.dropdown-toggle {
|
||||
a {
|
||||
display: inline-block;
|
||||
padding: 8px 15px;
|
||||
font-size: 14px;
|
||||
|
@ -145,18 +191,27 @@ body {
|
|||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.upload-buttons .btn {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
/** Tables */
|
||||
|
||||
table {
|
||||
table-layout: fixed;
|
||||
|
||||
.popover-title {
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
th, td {
|
||||
overflow: hidden;
|
||||
|
||||
// gcode files
|
||||
&.gcode_files_name {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
@ -309,6 +364,21 @@ ul.dropdown-menu li a {
|
|||
|
||||
#webcam_container {
|
||||
width: 100%;
|
||||
|
||||
.flipH {
|
||||
-webkit-transform: scaleX(-1);
|
||||
-moz-transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.flipV {
|
||||
-webkit-transform: scaleY(-1);
|
||||
-moz-transform: scaleY(-1);
|
||||
}
|
||||
|
||||
.flipH.flipV {
|
||||
-webkit-transform: scaleX(-1) scaleY(-1);
|
||||
-moz-transform: scaleX(-1) scaleY(-1);
|
||||
}
|
||||
}
|
||||
|
||||
/** GCODE file manager */
|
||||
|
@ -321,6 +391,10 @@ ul.dropdown-menu li a {
|
|||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** Control tab */
|
||||
|
@ -371,21 +445,36 @@ ul.dropdown-menu li a {
|
|||
|
||||
}
|
||||
|
||||
/** Terminal output */
|
||||
|
||||
#term {
|
||||
#terminal-output {
|
||||
min-height: 340px;
|
||||
}
|
||||
}
|
||||
|
||||
/** Settings dialog */
|
||||
#settings_dialog {
|
||||
}
|
||||
|
||||
/** Footer */
|
||||
.footer {
|
||||
text-align: right;
|
||||
ul {
|
||||
margin: 0;
|
||||
|
||||
ul li {
|
||||
display: inline;
|
||||
margin-left: 1em;
|
||||
font-size: 85%;
|
||||
a {
|
||||
color: #555;
|
||||
li {
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
display: inline;
|
||||
margin-left: 1em;
|
||||
font-size: 85%;
|
||||
a {
|
||||
color: #555;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -399,3 +488,133 @@ ul.dropdown-menu li a {
|
|||
overflow: visible !important;
|
||||
}
|
||||
|
||||
#drop_overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 10000;
|
||||
display: none;
|
||||
|
||||
&.in {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#drop_overlay_background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #000000;
|
||||
filter:alpha(opacity=50);
|
||||
-moz-opacity:0.5;
|
||||
-khtml-opacity: 0.5;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#drop_overlay_wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding-top: 60px;
|
||||
|
||||
@dropzone_width: 400px;
|
||||
@dropzone_height: 400px;
|
||||
@dropzone_distance: 50px;
|
||||
@dropzone_border: 2px;
|
||||
|
||||
#drop, #drop_background {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-left: -1 * @dropzone_width / 2;
|
||||
margin-top: -1 * @dropzone_height / 2;
|
||||
}
|
||||
|
||||
#drop_locally, #drop_locally_background {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-left: -1 * @dropzone_width - @dropzone_distance / 2;
|
||||
margin-top: -1 * @dropzone_height / 2;
|
||||
}
|
||||
|
||||
#drop_sd, #drop_sd_background {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-left: @dropzone_distance / 2;
|
||||
margin-top: -1 * @dropzone_height / 2;
|
||||
}
|
||||
|
||||
.dropzone {
|
||||
width: @dropzone_width + 2 * @dropzone_border;
|
||||
height: @dropzone_height + 2 * @dropzone_border;
|
||||
z-index: 10001;
|
||||
|
||||
color: #ffffff;
|
||||
font-size: 30px;
|
||||
|
||||
i {
|
||||
font-size: 50px;
|
||||
}
|
||||
|
||||
.centered {
|
||||
display: table-cell;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
width: @dropzone_width;
|
||||
height: @dropzone_height;
|
||||
line-height: 40px;
|
||||
|
||||
filter:alpha(opacity=100);
|
||||
-moz-opacity:1.0;
|
||||
-khtml-opacity: 1.0;
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
.dropzone_background {
|
||||
width: @dropzone_width;
|
||||
height: @dropzone_height;
|
||||
border: 2px dashed #eeeeee;
|
||||
-webkit-border-radius: 10px;
|
||||
-moz-border-radius: 10px;
|
||||
border-radius: 10px;
|
||||
|
||||
background-color: #000000;
|
||||
filter:alpha(opacity=25);
|
||||
-moz-opacity:0.25;
|
||||
-khtml-opacity: 0.25;
|
||||
opacity: 0.25;
|
||||
|
||||
&.hover {
|
||||
background-color: #000000;
|
||||
filter:alpha(opacity=50);
|
||||
-moz-opacity:0.5;
|
||||
-khtml-opacity: 0.5;
|
||||
opacity: 0.5;
|
||||
|
||||
}
|
||||
|
||||
&.fade {
|
||||
-webkit-transition: all 0.3s ease-out;
|
||||
-moz-transition: all 0.3s ease-out;
|
||||
-ms-transition: all 0.3s ease-out;
|
||||
-o-transition: all 0.3s ease-out;
|
||||
transition: all 0.3s ease-out;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon-sd-black-14 {
|
||||
background: url("../img/icon-sd-black-14.png") 0 3px no-repeat;
|
||||
width: 11px;
|
||||
height: 17px;
|
||||
}
|
||||
|
|
|
@ -223,42 +223,31 @@
|
|||
var assumeNonDC = false;
|
||||
|
||||
for(var i=0;i<gcode.length;i++){
|
||||
// for(var len = gcode.length- 1, i=0;i!=len;i++){
|
||||
x=undefined;
|
||||
y=undefined;
|
||||
z=undefined;
|
||||
retract = 0;
|
||||
|
||||
var line = gcode[i].line;
|
||||
var percentage = gcode[i].percentage;
|
||||
|
||||
extrude=false;
|
||||
gcode[i] = gcode[i].split(/[\(;]/)[0];
|
||||
line = line.split(/[\(;]/)[0];
|
||||
|
||||
// prevRetract=0;
|
||||
// retract=0;
|
||||
// if(gcode[i].match(/^(?:G0|G1)\s+/i)){
|
||||
if(reg.test(gcode[i])){
|
||||
var args = gcode[i].split(/\s/);
|
||||
if(reg.test(line)){
|
||||
var args = line.split(/\s/);
|
||||
for(j=0;j<args.length;j++){
|
||||
// console.log(args);
|
||||
// if(!args[j])continue;
|
||||
switch(argChar = args[j].charAt(0).toLowerCase()){
|
||||
case 'x':
|
||||
x=args[j].slice(1);
|
||||
// if(x === prevX){
|
||||
// x=undefined;
|
||||
// }
|
||||
break;
|
||||
case 'y':
|
||||
y=args[j].slice(1);
|
||||
// if(y===prevY){
|
||||
// y=undefined;
|
||||
// }
|
||||
break;
|
||||
case 'z':
|
||||
z=args[j].slice(1);
|
||||
z = Number(z);
|
||||
if(z == prevZ)continue;
|
||||
// z = Number(z);
|
||||
if(z == prevZ) continue;
|
||||
if(z_heights.hasOwnProperty(z)){
|
||||
layer = z_heights[z];
|
||||
}else{
|
||||
|
@ -267,9 +256,6 @@
|
|||
}
|
||||
sendLayer = layer;
|
||||
sendLayerZ = z;
|
||||
// if(parseFloat(prevZ) < )
|
||||
// if(args[j].charAt(1) === "-")layer--;
|
||||
// else layer++;
|
||||
prevZ = z;
|
||||
break;
|
||||
case 'e':
|
||||
|
@ -292,13 +278,11 @@
|
|||
retract = -1;
|
||||
}
|
||||
else if(prev_extrude["abs"]==0){
|
||||
// if(prevRetract <0 )prevRetract=retract;
|
||||
retract = 0;
|
||||
}else if(prev_extrude["abs"]>0&&prevRetract < 0){
|
||||
prevRetract = 0;
|
||||
retract = 1;
|
||||
} else {
|
||||
// prevRetract = retract;
|
||||
retract = 0;
|
||||
}
|
||||
prev_extrude[argChar] = numSlice;
|
||||
|
@ -317,24 +301,24 @@
|
|||
prev_extrude["abs"] = Math.sqrt((prevX-x)*(prevX-x)+(prevY-y)*(prevY-y));
|
||||
}
|
||||
if(!model[layer])model[layer]=[];
|
||||
if(typeof(x) !== 'undefined' || typeof(y) !== 'undefined' ||typeof(z) !== 'undefined'||retract!=0) model[layer][model[layer].length] = {x: Number(x), y: Number(y), z: Number(z), extrude: extrude, retract: Number(retract), noMove: false, extrusion: (extrude||retract)?Number(prev_extrude["abs"]):0, prevX: Number(prevX), prevY: Number(prevY), prevZ: Number(prevZ), speed: Number(lastF), gcodeLine: Number(i)};
|
||||
if(typeof(x) !== 'undefined' || typeof(y) !== 'undefined' ||typeof(z) !== 'undefined'||retract!=0) model[layer][model[layer].length] = {x: Number(x), y: Number(y), z: Number(z), extrude: extrude, retract: Number(retract), noMove: false, extrusion: (extrude||retract)?Number(prev_extrude["abs"]):0, prevX: Number(prevX), prevY: Number(prevY), prevZ: Number(prevZ), speed: Number(lastF), gcodeLine: Number(i), percentage: percentage};
|
||||
//{x: x, y: y, z: z, extrude: extrude, retract: retract, noMove: false, extrusion: (extrude||retract)?prev_extrude["abs"]:0, prevX: prevX, prevY: prevY, prevZ: prevZ, speed: lastF, gcodeLine: i};
|
||||
if(typeof(x) !== 'undefined') prevX = x;
|
||||
if(typeof(y) !== 'undefined') prevY = y;
|
||||
} else if(gcode[i].match(/^(?:M82)/i)){
|
||||
} else if(line.match(/^(?:M82)/i)){
|
||||
extrudeRelative = false;
|
||||
}else if(gcode[i].match(/^(?:G91)/i)){
|
||||
}else if(line.match(/^(?:G91)/i)){
|
||||
extrudeRelative=true;
|
||||
}else if(gcode[i].match(/^(?:G90)/i)){
|
||||
}else if(line.match(/^(?:G90)/i)){
|
||||
extrudeRelative=false;
|
||||
}else if(gcode[i].match(/^(?:M83)/i)){
|
||||
}else if(line.match(/^(?:M83)/i)){
|
||||
extrudeRelative=true;
|
||||
}else if(gcode[i].match(/^(?:M101)/i)){
|
||||
}else if(line.match(/^(?:M101)/i)){
|
||||
dcExtrude=true;
|
||||
}else if(gcode[i].match(/^(?:M103)/i)){
|
||||
}else if(line.match(/^(?:M103)/i)){
|
||||
dcExtrude=false;
|
||||
}else if(gcode[i].match(/^(?:G92)/i)){
|
||||
var args = gcode[i].split(/\s/);
|
||||
}else if(line.match(/^(?:G92)/i)){
|
||||
var args = line.split(/\s/);
|
||||
for(j=0;j<args.length;j++){
|
||||
switch(argChar = args[j].charAt(0).toLowerCase()){
|
||||
case 'x':
|
||||
|
@ -354,16 +338,15 @@
|
|||
else {
|
||||
prev_extrude[argChar] = numSlice;
|
||||
}
|
||||
// prevZ = z;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!model[layer])model[layer]=[];
|
||||
if(typeof(x) !== 'undefined' || typeof(y) !== 'undefined' ||typeof(z) !== 'undefined') model[layer][model[layer].length] = {x: parseFloat(x), y: parseFloat(y), z: parseFloat(z), extrude: extrude, retract: parseFloat(retract), noMove: true, extrusion: (extrude||retract)?parseFloat(prev_extrude["abs"]):0, prevX: parseFloat(prevX), prevY: parseFloat(prevY), prevZ: parseFloat(prevZ), speed: parseFloat(lastF),gcodeLine: parseFloat(i)};
|
||||
}else if(gcode[i].match(/^(?:G28)/i)){
|
||||
var args = gcode[i].split(/\s/);
|
||||
if(typeof(x) !== 'undefined' || typeof(y) !== 'undefined' ||typeof(z) !== 'undefined') model[layer][model[layer].length] = {x: parseFloat(x), y: parseFloat(y), z: parseFloat(z), extrude: extrude, retract: parseFloat(retract), noMove: true, extrusion: (extrude||retract)?parseFloat(prev_extrude["abs"]):0, prevX: parseFloat(prevX), prevY: parseFloat(prevY), prevZ: parseFloat(prevZ), speed: parseFloat(lastF),gcodeLine: parseFloat(i), percentage: percentage};
|
||||
}else if(line.match(/^(?:G28)/i)){
|
||||
var args = line.split(/\s/);
|
||||
for(j=0;j<args.length;j++){
|
||||
switch(argChar = args[j].charAt(0).toLowerCase()){
|
||||
case 'x':
|
||||
|
@ -405,17 +388,11 @@
|
|||
}
|
||||
prevZ = z;
|
||||
}
|
||||
// x=0, y=0,z=0,prevZ=0, extrude=false;
|
||||
// if(typeof(prevX) === 'undefined'){prevX=0;}
|
||||
// if(typeof(prevY) === 'undefined'){prevY=0;}
|
||||
|
||||
if(!model[layer])model[layer]=[];
|
||||
if(typeof(x) !== 'undefined' || typeof(y) !== 'undefined' ||typeof(z) !== 'undefined'||retract!=0) model[layer][model[layer].length] = {x: Number(x), y: Number(y), z: Number(z), extrude: extrude, retract: Number(retract), noMove: false, extrusion: (extrude||retract)?Number(prev_extrude["abs"]):0, prevX: Number(prevX), prevY: Number(prevY), prevZ: Number(prevZ), speed: Number(lastF), gcodeLine: Number(i)};
|
||||
// if(typeof(x) !== 'undefined' || typeof(y) !== 'undefined' ||typeof(z) !== 'undefined') model[layer][model[layer].length] = {x: x, y: y, z: z, extrude: extrude, retract: retract, noMove:false, extrusion: (extrude||retract)?prev_extrude["abs"]:0, prevX: prevX, prevY: prevY, prevZ: prevZ, speed: lastF, gcodeLine: parseFloat(i)};
|
||||
if(typeof(x) !== 'undefined' || typeof(y) !== 'undefined' ||typeof(z) !== 'undefined'||retract!=0) model[layer][model[layer].length] = {x: Number(x), y: Number(y), z: Number(z), extrude: extrude, retract: Number(retract), noMove: false, extrusion: (extrude||retract)?Number(prev_extrude["abs"]):0, prevX: Number(prevX), prevY: Number(prevY), prevZ: Number(prevZ), speed: Number(lastF), gcodeLine: Number(i), percentage: percentage};
|
||||
}
|
||||
if(typeof(sendLayer) !== "undefined"){
|
||||
// sendLayerToParent(sendLayer, sendLayerZ, i/gcode.length*100);
|
||||
// sendLayer = undefined;
|
||||
|
||||
if(i-lastSend > gcode.length*0.02 && sendMultiLayer.length != 0){
|
||||
lastSend = i;
|
||||
|
@ -429,13 +406,7 @@
|
|||
sendLayerZ = undefined;
|
||||
}
|
||||
}
|
||||
// sendMultiLayer[sendMultiLayer.length] = layer;
|
||||
// sendMultiLayerZ[sendMultiLayerZ.length] = z;
|
||||
sendMultiLayerToParent(sendMultiLayer, sendMultiLayerZ, i/gcode.length*100);
|
||||
|
||||
// if(gCodeOptions["sortLayers"])sortLayers();
|
||||
// if(gCodeOptions["purgeEmptyLayers"])purgeLayers();
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -23,57 +23,73 @@ GCODE.gCodeReader = (function(){
|
|||
purgeEmptyLayers: true,
|
||||
analyzeModel: false
|
||||
};
|
||||
var linesCmdIndex = {};
|
||||
|
||||
var prepareGCode = function(){
|
||||
var percentageTree = undefined;
|
||||
|
||||
var prepareGCode = function(totalSize){
|
||||
if(!lines)return;
|
||||
gcode = [];
|
||||
var i, tmp;
|
||||
var i, tmp, byteCount;
|
||||
|
||||
byteCount = 0;
|
||||
for(i=0;i<lines.length;i++){
|
||||
// if(lines[i].match(/^(G0|G1|G90|G91|G92|M82|M83|G28)/i))gcode.push(lines[i]);
|
||||
byteCount += lines[i].length + 1; // line length + \n
|
||||
tmp = lines[i].indexOf(";");
|
||||
if(tmp > 1 || tmp === -1) {
|
||||
gcode.push(lines[i]);
|
||||
gcode.push({line: lines[i], percentage: byteCount * 100 / totalSize});
|
||||
}
|
||||
}
|
||||
lines = [];
|
||||
// console.log("GCode prepared");
|
||||
};
|
||||
|
||||
var sortLayers = function(){
|
||||
var sortedZ = [];
|
||||
var tmpModel = [];
|
||||
// var cnt = 0;
|
||||
// console.log(z_heights);
|
||||
|
||||
for(var layer in z_heights){
|
||||
sortedZ[z_heights[layer]] = layer;
|
||||
// cnt++;
|
||||
}
|
||||
// console.log("cnt is " + cnt);
|
||||
|
||||
sortedZ.sort(function(a,b){
|
||||
return a-b;
|
||||
});
|
||||
// console.log(sortedZ);
|
||||
// console.log(model.length);
|
||||
|
||||
for(var i=0;i<sortedZ.length;i++){
|
||||
// console.log("i is " + i +" and sortedZ[i] is " + sortedZ[i] + "and z_heights[] is " + z_heights[sortedZ[i]] );
|
||||
if(typeof(z_heights[sortedZ[i]]) === 'undefined')continue;
|
||||
tmpModel[i] = model[z_heights[sortedZ[i]]];
|
||||
}
|
||||
model = tmpModel;
|
||||
// console.log(model.length);
|
||||
delete tmpModel;
|
||||
};
|
||||
|
||||
var prepareLinesIndex = function(){
|
||||
linesCmdIndex = {};
|
||||
percentageTree = undefined;
|
||||
|
||||
for (var l in model){
|
||||
for (var i=0; i< model[l].length; i++){
|
||||
linesCmdIndex[model[l][i].gcodeLine] = {layer: l, cmd: i};
|
||||
for (var l in model) {
|
||||
for (var i=0; i< model[l].length; i++) {
|
||||
var percentage = model[l][i].percentage;
|
||||
var value = {layer: l, cmd: i};
|
||||
if (!percentageTree) {
|
||||
percentageTree = new AVLTree({key: percentage, value: value}, "key");
|
||||
} else {
|
||||
percentageTree.add({key: percentage, value: value});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var searchInPercentageTree = function(key) {
|
||||
if (percentageTree === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
var elements = percentageTree.findBest(key);
|
||||
if (elements.length == 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return elements[0];
|
||||
};
|
||||
|
||||
var purgeLayers = function(){
|
||||
var purge=true;
|
||||
|
@ -102,13 +118,13 @@ GCODE.gCodeReader = (function(){
|
|||
return {
|
||||
|
||||
loadFile: function(reader){
|
||||
// console.log("loadFile");
|
||||
model = [];
|
||||
z_heights = [];
|
||||
|
||||
var totalSize = reader.target.result.length;
|
||||
lines = reader.target.result.split(/\n/);
|
||||
reader.target.result = null;
|
||||
prepareGCode();
|
||||
prepareGCode(totalSize);
|
||||
|
||||
worker.postMessage({
|
||||
"cmd":"parseGCode",
|
||||
|
@ -129,32 +145,21 @@ GCODE.gCodeReader = (function(){
|
|||
}
|
||||
},
|
||||
passDataToRenderer: function(){
|
||||
// console.log(model);
|
||||
if(gCodeOptions["sortLayers"])sortLayers();
|
||||
// console.log(model);
|
||||
if(gCodeOptions["purgeEmptyLayers"])purgeLayers();
|
||||
prepareLinesIndex();
|
||||
// console.log(model);
|
||||
GCODE.renderer.doRender(model, 0);
|
||||
// GCODE.renderer3d.setModel(model);
|
||||
|
||||
},
|
||||
processLayerFromWorker: function(msg){
|
||||
// var cmds = msg.cmds;
|
||||
// var layerNum = msg.layerNum;
|
||||
// var zHeightObject = msg.zHeightObject;
|
||||
// var isEmpty = msg.isEmpty;
|
||||
// console.log(zHeightObject);
|
||||
model[msg.layerNum] = msg.cmds;
|
||||
z_heights[msg.zHeightObject.zValue] = msg.zHeightObject.layer;
|
||||
// GCODE.renderer.doRender(model, msg.layerNum);
|
||||
},
|
||||
processMultiLayerFromWorker: function(msg){
|
||||
for(var i=0;i<msg.layerNum.length;i++){
|
||||
model[msg.layerNum[i]] = msg.model[msg.layerNum[i]];
|
||||
z_heights[msg.zHeightObject.zValue[i]] = msg.layerNum[i];
|
||||
}
|
||||
// console.log(model);
|
||||
},
|
||||
processAnalyzeModelDone: function(msg){
|
||||
min = msg.min;
|
||||
|
@ -190,8 +195,13 @@ GCODE.gCodeReader = (function(){
|
|||
var result = {first: model[layer][fromSegments].gcodeLine, last: model[layer][toSegments].gcodeLine};
|
||||
return result;
|
||||
},
|
||||
getLinesCmdIndex: function(line){
|
||||
return linesCmdIndex[line];
|
||||
getCmdIndexForPercentage: function(percentage) {
|
||||
var command = searchInPercentageTree(percentage);
|
||||
if (command === undefined) {
|
||||
return undefined
|
||||
} else {
|
||||
return command.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}());
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 197 B |
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 |
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
@ -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";
|
||||
})
|
||||
}
|
|
@ -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"}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,222 @@
|
|||
function TemperatureViewModel(loginStateViewModel, settingsViewModel) {
|
||||
var self = this;
|
||||
|
||||
self.loginState = loginStateViewModel;
|
||||
|
||||
self.temp = ko.observable(undefined);
|
||||
self.bedTemp = ko.observable(undefined);
|
||||
self.targetTemp = ko.observable(undefined);
|
||||
self.bedTargetTemp = ko.observable(undefined);
|
||||
|
||||
self.newTemp = ko.observable(undefined);
|
||||
self.newBedTemp = ko.observable(undefined);
|
||||
|
||||
self.newTempOffset = ko.observable(undefined);
|
||||
self.tempOffset = ko.observable(0);
|
||||
self.newBedTempOffset = ko.observable(undefined);
|
||||
self.bedTempOffset = ko.observable(0);
|
||||
|
||||
self.isErrorOrClosed = ko.observable(undefined);
|
||||
self.isOperational = ko.observable(undefined);
|
||||
self.isPrinting = ko.observable(undefined);
|
||||
self.isPaused = ko.observable(undefined);
|
||||
self.isError = ko.observable(undefined);
|
||||
self.isReady = ko.observable(undefined);
|
||||
self.isLoading = ko.observable(undefined);
|
||||
|
||||
self.temperature_profiles = settingsViewModel.temperature_profiles;
|
||||
|
||||
self.tempString = ko.computed(function() {
|
||||
if (!self.temp())
|
||||
return "-";
|
||||
return self.temp() + " °C";
|
||||
});
|
||||
self.bedTempString = ko.computed(function() {
|
||||
if (!self.bedTemp())
|
||||
return "-";
|
||||
return self.bedTemp() + " °C";
|
||||
});
|
||||
self.targetTempString = ko.computed(function() {
|
||||
if (!self.targetTemp())
|
||||
return "-";
|
||||
return self.targetTemp() + " °C";
|
||||
});
|
||||
self.bedTargetTempString = ko.computed(function() {
|
||||
if (!self.bedTargetTemp())
|
||||
return "-";
|
||||
return self.bedTargetTemp() + " °C";
|
||||
});
|
||||
|
||||
self.temperatures = [];
|
||||
self.plotOptions = {
|
||||
yaxis: {
|
||||
min: 0,
|
||||
max: 310,
|
||||
ticks: 10
|
||||
},
|
||||
xaxis: {
|
||||
mode: "time",
|
||||
minTickSize: [2, "minute"],
|
||||
tickFormatter: function(val, axis) {
|
||||
if (val == undefined || val == 0)
|
||||
return ""; // we don't want to display the minutes since the epoch if not connected yet ;)
|
||||
|
||||
// current time in milliseconds in UTC
|
||||
var timestampUtc = Date.now();
|
||||
|
||||
// calculate difference in milliseconds
|
||||
var diff = timestampUtc - val;
|
||||
|
||||
// convert to minutes
|
||||
var diffInMins = Math.round(diff / (60 * 1000));
|
||||
if (diffInMins == 0)
|
||||
return "just now";
|
||||
else
|
||||
return "- " + diffInMins + " min";
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
noColumns: 4
|
||||
}
|
||||
}
|
||||
|
||||
self.fromCurrentData = function(data) {
|
||||
self._processStateData(data.state);
|
||||
self._processTemperatureUpdateData(data.temperatures);
|
||||
self._processOffsetData(data.offsets);
|
||||
}
|
||||
|
||||
self.fromHistoryData = function(data) {
|
||||
self._processStateData(data.state);
|
||||
self._processTemperatureHistoryData(data.temperatureHistory);
|
||||
self._processOffsetData(data.offsets);
|
||||
}
|
||||
|
||||
self._processStateData = function(data) {
|
||||
self.isErrorOrClosed(data.flags.closedOrError);
|
||||
self.isOperational(data.flags.operational);
|
||||
self.isPaused(data.flags.paused);
|
||||
self.isPrinting(data.flags.printing);
|
||||
self.isError(data.flags.error);
|
||||
self.isReady(data.flags.ready);
|
||||
self.isLoading(data.flags.loading);
|
||||
}
|
||||
|
||||
self._processTemperatureUpdateData = function(data) {
|
||||
if (data.length == 0)
|
||||
return;
|
||||
|
||||
self.temp(data[data.length - 1].temp);
|
||||
self.bedTemp(data[data.length - 1].bedTemp);
|
||||
self.targetTemp(data[data.length - 1].targetTemp);
|
||||
self.bedTargetTemp(data[data.length - 1].targetBedTemp);
|
||||
|
||||
if (!CONFIG_TEMPERATURE_GRAPH) return;
|
||||
|
||||
if (!self.temperatures)
|
||||
self.temperatures = [];
|
||||
if (!self.temperatures.actual)
|
||||
self.temperatures.actual = [];
|
||||
if (!self.temperatures.target)
|
||||
self.temperatures.target = [];
|
||||
if (!self.temperatures.actualBed)
|
||||
self.temperatures.actualBed = [];
|
||||
if (!self.temperatures.targetBed)
|
||||
self.temperatures.targetBed = [];
|
||||
|
||||
_.each(data, function(d) {
|
||||
var time = d.currentTime;
|
||||
self.temperatures.actual.push([time, d.temp]);
|
||||
self.temperatures.target.push([time, d.targetTemp]);
|
||||
self.temperatures.actualBed.push([time, d.bedTemp]);
|
||||
self.temperatures.targetBed.push([time, d.targetBedTemp]);
|
||||
});
|
||||
|
||||
self.temperatures.actual = self.temperatures.actual.slice(-300);
|
||||
self.temperatures.target = self.temperatures.target.slice(-300);
|
||||
self.temperatures.actualBed = self.temperatures.actualBed.slice(-300);
|
||||
self.temperatures.targetBed = self.temperatures.targetBed.slice(-300);
|
||||
|
||||
self.updatePlot();
|
||||
}
|
||||
|
||||
self._processTemperatureHistoryData = function(data) {
|
||||
self.temperatures = data;
|
||||
self.updatePlot();
|
||||
}
|
||||
|
||||
self._processOffsetData = function(data) {
|
||||
self.tempOffset(data[0]);
|
||||
self.bedTempOffset(data[1]);
|
||||
}
|
||||
|
||||
self.updatePlot = function() {
|
||||
var graph = $("#temperature-graph");
|
||||
if (graph.length) {
|
||||
var data = [
|
||||
{label: "Actual", color: "#FF4040", data: self.temperatures.actual},
|
||||
{label: "Target", color: "#FFA0A0", data: self.temperatures.target},
|
||||
{label: "Bed Actual", color: "#4040FF", data: self.temperatures.actualBed},
|
||||
{label: "Bed Target", color: "#A0A0FF", data: self.temperatures.targetBed}
|
||||
]
|
||||
|
||||
$.plot(graph, data, self.plotOptions);
|
||||
}
|
||||
}
|
||||
|
||||
self.setTempFromProfile = function(profile) {
|
||||
if (!profile)
|
||||
return;
|
||||
self._updateTemperature(profile.extruder, "temp");
|
||||
}
|
||||
|
||||
self.setTemp = function() {
|
||||
self._updateTemperature(self.newTemp(), "temp", function(){self.targetTemp(self.newTemp()); self.newTemp("");});
|
||||
};
|
||||
|
||||
self.setTempToZero = function() {
|
||||
self._updateTemperature(0, "temp", function(){self.targetTemp(0); self.newTemp("");});
|
||||
}
|
||||
|
||||
self.setTempOffset = function() {
|
||||
self._updateTemperature(self.newTempOffset(), "tempOffset", function() {self.tempOffset(self.newTempOffset()); self.newTempOffset("");});
|
||||
}
|
||||
|
||||
self.setBedTempFromProfile = function(profile) {
|
||||
self._updateTemperature(profile.bed, "bedTemp");
|
||||
}
|
||||
|
||||
self.setBedTemp = function() {
|
||||
self._updateTemperature(self.newBedTemp(), "bedTemp", function() {self.bedTargetTemp(self.newBedTemp()); self.newBedTemp("");});
|
||||
};
|
||||
|
||||
self.setBedTempToZero = function() {
|
||||
self._updateTemperature(0, "bedTemp", function() {self.bedTargetTemp(0); self.newBedTemp("");});
|
||||
}
|
||||
|
||||
self.setBedTempOffset = function() {
|
||||
self._updateTemperature(self.newBedTempOffset(), "bedTempOffset", function() {self.bedTempOffset(self.newBedTempOffset()); self.newBedTempOffset("");});
|
||||
}
|
||||
|
||||
self._updateTemperature = function(temp, type, callback) {
|
||||
var data = {};
|
||||
data[type] = temp;
|
||||
|
||||
$.ajax({
|
||||
url: AJAX_BASEURL + "control/temperature",
|
||||
type: "POST",
|
||||
data: data,
|
||||
success: function() { if (callback !== undefined) callback(); }
|
||||
});
|
||||
}
|
||||
|
||||
self.handleEnter = function(event, type) {
|
||||
if (event.keyCode == 13) {
|
||||
if (type == "temp") {
|
||||
self.setTemp();
|
||||
} else if (type == "bedTemp") {
|
||||
self.setBedTemp();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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
Binary file not shown.
Binary file not shown.
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
|
@ -0,0 +1,2 @@
|
|||
User-agent: *
|
||||
Disallow: /
|
|
@ -1,4 +1,4 @@
|
|||
<div id="offline_overlay">
|
||||
<div id="offline_overlay" xmlns="http://www.w3.org/1999/html">
|
||||
<div id="offline_overlay_background"></div>
|
||||
<div id="offline_overlay_wrapper">
|
||||
<div class="container">
|
||||
|
@ -17,6 +17,21 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="drop_overlay" data-bind="visible: loginState.isUser()">
|
||||
<div id="drop_overlay_background"></div>
|
||||
<div id="drop_overlay_wrapper">
|
||||
{% if enableSdSupport %}
|
||||
<div class="dropzone" id="drop_locally"><span class="centered"><i class="icon-upload-alt"></i><br>Upload locally</span></div>
|
||||
<div class="dropzone_background" id="drop_locally_background"></div>
|
||||
<div class="dropzone" id="drop_sd"><span class="centered"><i class="icon-upload-alt"></i><br>Upload to SD<br><small data-bind="visible: !isSdReady()">(SD not initialized)</small></span></div>
|
||||
<div class="dropzone_background" id="drop_sd_background"></div>
|
||||
{% else %}
|
||||
<div class="dropzone" id="drop"><span class="centered"><i class="icon-upload-alt"></i><br>Upload</span></div>
|
||||
<div class="dropzone_background" id="drop_background"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="confirmation_dialog" class="modal hide fade">
|
||||
<div class="modal-header">
|
||||
<a href="#" class="close" data-dismiss="modal" aria-hidden="true">×</a>
|
||||
|
@ -30,4 +45,59 @@
|
|||
<a href="#" class="btn" data-dismiss="modal" aria-hidden="true">Cancel</a>
|
||||
<a href="#" class="btn btn-danger confirmation_dialog_acknowledge">Proceed</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="first_run_dialog" class="modal hide fade" data-backdrop="static" data-keyboard="false">
|
||||
<div class="modal-header">
|
||||
<h3><i class="icon-warning-sign"></i> Configure Access Control</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
<strong>Please read the following, it is very important for your printer's health!</strong>
|
||||
</p>
|
||||
<p>
|
||||
OctoPrint by default now ships with Access Control enabled, meaning you won't be able to do anything with the
|
||||
printer unless you login first as a configured user. This is to <strong>prevent strangers - possibly with
|
||||
malicious intent - to gain access to your printer</strong> via the internet or another untrustworthy network
|
||||
and using it in such a way that it is damaged or worse (i.e. causes a fire).
|
||||
</p>
|
||||
<p>
|
||||
It looks like you haven't configured access control yet. Please <strong>set up an username and password</strong> for the
|
||||
initial administrator account who will have full access to both the printer and OctoPrint's settings, then click
|
||||
on "Keep Access Control Enabled":
|
||||
</p>
|
||||
<form class="form-horizontal">
|
||||
<div class="control-group" data-bind="css: {success: validUsername()}">
|
||||
<label class="control-label" for="first_run_username">Username</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-medium" data-bind="value: username, valueUpdate: 'afterkeydown'">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group" data-bind="css: {success: validPassword()}">
|
||||
<label class="control-label" for="first_run_username">Password</label>
|
||||
<div class="controls">
|
||||
<input type="password" class="input-medium" data-bind="value: password, valueUpdate: 'afterkeydown'">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group" data-bind="css: {error: passwordMismatch(), success: validPassword() && !passwordMismatch()}">
|
||||
<label class="control-label" for="first_run_username">Confirm Password</label>
|
||||
<div class="controls">
|
||||
<input type="password" class="input-medium" data-bind="value: confirmedPassword, valueUpdate: 'afterkeydown'">
|
||||
<span class="help-inline" data-bind="visible: passwordMismatch()">Passwords don't match</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<p>
|
||||
<strong>Note:</strong> In case that your OctoPrint installation is only accessible from within a trustworthy network and you don't
|
||||
need Access Control for other reasons, you may alternatively disable Access Control. You should only
|
||||
do this if you are absolutely certain that only people you know and trust will be able to connect to it.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Do NOT underestimate the risk of an unsecured access from the internet to your printer!</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="#" class="btn btn-danger" data-bind="click: disableAccessControl">Disable Access Control</a>
|
||||
<a href="#" class="btn btn-primary" data-bind="click: keepAccessControl, enable: validData(), css: {disabled: !validData()}">Keep Access Control Enabled</a>
|
||||
</div>
|
||||
</div>
|
|
@ -24,17 +24,20 @@
|
|||
var CONFIG_USERSPERPAGE = 10;
|
||||
var CONFIG_WEBCAM_STREAM = "{{ webcamStream }}";
|
||||
var CONFIG_ACCESS_CONTROL = {% if enableAccessControl -%} true; {% else %} false; {%- endif %}
|
||||
var CONFIG_SD_SUPPORT = {% if enableSdSupport -%} true; {% else %} false; {%- endif %}
|
||||
var CONFIG_FIRST_RUN = {% if firstRun -%} true; {% else %} false; {%- endif %}
|
||||
var CONFIG_TEMPERATURE_GRAPH = {% if enableTemperatureGraph -%} true; {% else %} false; {%- endif %}
|
||||
|
||||
var WEB_SOCKET_SWF_LOCATION = "{{ url_for('static', filename='js/socket.io/WebSocketMain.swf') }}";
|
||||
var WEB_SOCKET_DEBUG = true;
|
||||
var SOCKJS_URI = window.location.protocol.slice(0, -1) + "://" + (window.document ? window.document.domain : window.location.hostname) + ":" + window.location.port + "/sockjs";
|
||||
var SOCKJS_DEBUG = {% if debug -%} true; {% else %} false; {%- endif %}
|
||||
</script>
|
||||
<script src="{{ url_for('static', filename='js/less-1.3.3.min.js') }}" type="text/javascript"></script>
|
||||
<script src="{{ url_for('static', filename='js/lib/less-1.3.3.min.js') }}" type="text/javascript"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="navbar" class="navbar navbar-fixed-top">
|
||||
<div class="navbar-inner" data-bind="css: appearance.color">
|
||||
<div class="container">
|
||||
<a class="brand" href="#"><img src="{{ url_for('static', filename='img/tentacle-20x20.png') }}"> <span data-bind="text: appearance.brand">OctoPrint</span></a>
|
||||
<a class="brand" href="#"> <span data-bind="text: appearance.brand">OctoPrint</span></a>
|
||||
<div class="nav-collapse">
|
||||
<ul class="nav pull-right">
|
||||
<li style="display: none;" data-bind="visible: loginState.isAdmin">
|
||||
|
@ -89,14 +92,17 @@
|
|||
</div>
|
||||
<div class="accordion-body collapse in" id="connection">
|
||||
<div class="accordion-inner">
|
||||
<label for="connection_ports" data-bind="css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed && loginState.isUser">Serial Port</label>
|
||||
<select id="connection_ports" data-bind="options: portOptions, optionsCaption: 'AUTO', value: selectedPort, css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed && loginState.isUser"></select>
|
||||
<label for="connection_baudrates" data-bind="css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed && loginState.isUser">Baudrate</label>
|
||||
<select id="connection_baudrates" data-bind="options: baudrateOptions, optionsCaption: 'AUTO', value: selectedBaudrate, css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed && loginState.isUser"></select>
|
||||
<label for="connection_ports" data-bind="css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()">Serial Port</label>
|
||||
<select id="connection_ports" data-bind="options: portOptions, optionsCaption: 'AUTO', value: selectedPort, css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()"></select>
|
||||
<label for="connection_baudrates" data-bind="css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()">Baudrate</label>
|
||||
<select id="connection_baudrates" data-bind="options: baudrateOptions, optionsCaption: 'AUTO', value: selectedBaudrate, css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()"></select>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="connection_save" data-bind="checked: saveSettings, css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed && loginState.isUser"> Save connection settings
|
||||
<input type="checkbox" id="connection_save" data-bind="checked: saveSettings, css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()"> Save connection settings
|
||||
</label>
|
||||
<button class="btn btn-block" id="printer_connect" data-bind="click: connect, text: buttonText(), enable: loginState.isUser">Connect</button>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="connection_autoconnect" data-bind="checked: settings.serial_autoconnect, css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()"> Auto-connect on server startup
|
||||
</label>
|
||||
<button class="btn btn-block" id="printer_connect" data-bind="click: connect, text: buttonText(), enable: loginState.isUser()">Connect</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -107,16 +113,17 @@
|
|||
<div class="accordion-body collapse in" id="state">
|
||||
<div class="accordion-inner">
|
||||
Machine State: <strong data-bind="text: stateString"></strong><br>
|
||||
File: <strong data-bind="text: filename"></strong><br>
|
||||
File: <strong data-bind="text: filename"></strong> <strong data-bind="visible: sd">(SD)</strong><br>
|
||||
Filament: <strong data-bind="text: filament"></strong><br>
|
||||
Estimated Print Time: <strong data-bind="text: estimatedPrintTime"></strong><br>
|
||||
Line: <strong data-bind="text: lineString"></strong><br>
|
||||
Height: <strong data-bind="text: currentHeight"></strong><br>
|
||||
Timelapse: <strong data-bind="text: timelapseString"></strong><br>
|
||||
Height: <strong data-bind="text: heightString"></strong><br>
|
||||
Print Time: <strong data-bind="text: printTime"></strong><br>
|
||||
Print Time Left: <strong data-bind="text: printTimeLeft"></strong><br>
|
||||
Printed: <strong data-bind="text: byteString"></strong><br>
|
||||
|
||||
<div class="progress">
|
||||
<div class="bar" id="job_progressBar" data-bind="style: { width: progress() + '%' }"></div>
|
||||
<div class="bar" id="job_progressBar" data-bind="style: { width: progressString() + '%' }"></div>
|
||||
</div>
|
||||
|
||||
<div class="row-fluid print-control" style="display: none;" data-bind="visible: loginState.isUser">
|
||||
|
@ -131,7 +138,7 @@
|
|||
<div class="accordion-heading">
|
||||
<a class="accordion-toggle" data-toggle="collapse" href="#files"><i class="icon-list"></i> Files</a>
|
||||
|
||||
<div class="settings-trigger btn-group">
|
||||
<div class="settings-trigger accordion-heading-button btn-group">
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<span class="icon-wrench"></span>
|
||||
</a>
|
||||
|
@ -139,10 +146,28 @@
|
|||
<li><a href="#" data-bind="click: function() { $root.listHelper.changeSorting('name'); }"><i class="icon-ok" data-bind="style: {visibility: listHelper.currentSorting() == 'name' ? 'visible' : 'hidden'}"></i> Sort by name (ascending)</a></li>
|
||||
<li><a href="#" data-bind="click: function() { $root.listHelper.changeSorting('upload'); }"><i class="icon-ok" data-bind="style: {visibility: listHelper.currentSorting() == 'upload' ? 'visible' : 'hidden'}"></i> Sort by upload date (descending)</a></li>
|
||||
<li><a href="#" data-bind="click: function() { $root.listHelper.changeSorting('size'); }"><i class="icon-ok" data-bind="style: {visibility: listHelper.currentSorting() == 'size' ? 'visible' : 'hidden'}"></i> Sort by file size (descending)</a></li>
|
||||
{% if enableSdSupport %}
|
||||
<li class="divider"></li>
|
||||
<li><a href="#" data-bind="click: function() { $root.listHelper.toggleFilter('local'); }"><i class="icon-ok" data-bind="style: {visibility: _.contains(listHelper.currentFilters(), 'local') ? 'visible' : 'hidden'}"></i> Only show files stored locally</a></li>
|
||||
<li><a href="#" data-bind="click: function() { $root.listHelper.toggleFilter('sd'); }"><i class="icon-ok" data-bind="style: {visibility: _.contains(listHelper.currentFilters(), 'sd') ? 'visible' : 'hidden'}"></i> Only show files stored on SD</a></li>
|
||||
{% endif %}
|
||||
<li class="divider"></li>
|
||||
<li><a href="#" data-bind="click: function() { $root.listHelper.toggleFilter('printed'); }"><i class="icon-ok" data-bind="style: {visibility: _.contains(listHelper.currentFilters(), 'printed') ? 'visible' : 'hidden'}"></i> Hide successfully printed files</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{% if enableSdSupport %}
|
||||
<div class="sd-trigger accordion-heading-button btn-group">
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<span class="icon-sd-black-14"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li data-bind="visible: !isSdReady()"><a href="#" data-bind="click: function() { $root.initSdCard(); }"><i class="icon-flag"></i> Initialize SD card</a></li>
|
||||
<li data-bind="visible: isSdReady()"><a href="#" data-bind="click: function() { $root.refreshSdFiles(); }"><i class="icon-refresh"></i> Refresh SD files</a></li>
|
||||
<li data-bind="visible: isSdReady()"><a href="#" data-bind="click: function() { $root.releaseSdCard(); }"><i class="icon-eject"></i> Release SD card</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="accordion-body collapse in overflow_visible" id="files">
|
||||
<div class="accordion-inner">
|
||||
|
@ -155,15 +180,18 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody data-bind="foreach: listHelper.paginatedItems">
|
||||
<tr data-bind="css: $root.getSuccessClass($data), popover: { title: name, animation: true, html: true, placement: 'right', trigger: 'hover', delay: 0, content: $root.getPopoverContent($data), html: true }">
|
||||
<tr data-bind="css: $root.getSuccessClass($data), style: { 'font-weight': $root.listHelper.isSelected($data) ? 'bold' : 'normal' }, popover: { title: name, animation: true, html: true, placement: 'right', trigger: 'hover', delay: 0, content: $root.getPopoverContent($data), html: true }">
|
||||
<td class="gcode_files_name" data-bind="text: name"></td>
|
||||
<td class="gcode_files_size" data-bind="text: size"></td>
|
||||
<td class="gcode_files_action">
|
||||
<a href="#" class="icon-trash" title="Remove" data-bind="click: function() { if ($root.loginState.isUser()) { $root.removeFile($data.name); } else { return; } }, css: {disabled: !$root.loginState.isUser()}"></a> | <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> | <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> | <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> | <a href="#" class="icon-print" title="Load and Print" data-bind="click: function() { if ($root.enableSelect($data)) { $root.loadFile($data.name, true); } else { return; } }, css: {disabled: !$root.enableSelect($data)}"></a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="muted text-right">
|
||||
<small>Free: <span data-bind="text: freeSpace"></span></small>
|
||||
</div>
|
||||
<div class="pagination pagination-mini pagination-centered">
|
||||
<ul>
|
||||
<li data-bind="css: {disabled: listHelper.currentPage() === 0}"><a href="#" data-bind="click: listHelper.prevPage">«</a></li>
|
||||
|
@ -176,11 +204,26 @@
|
|||
</ul>
|
||||
</div>
|
||||
<div style="display: none;" data-bind="visible: loginState.isUser">
|
||||
<span class="btn btn-primary btn-block fileinput-button" data-bind="css: {disabled: !$root.loginState.isUser()}" style="margin-bottom: 10px">
|
||||
<i class="icon-upload icon-white"></i>
|
||||
<span>Upload</span>
|
||||
<input id="gcode_upload" type="file" name="gcode_file" class="fileinput-button" data-url="/ajax/gcodefiles/upload" data-bind="enable: loginState.isUser()">
|
||||
</span>
|
||||
<div class="row-fluid upload-buttons">
|
||||
{% if enableSdSupport %}
|
||||
<span class="btn btn-primary fileinput-button span6" data-bind="css: {disabled: !$root.loginState.isUser()}" style="margin-bottom: 10px">
|
||||
<i class="icon-upload-alt icon-white"></i>
|
||||
<span>Upload</span>
|
||||
<input id="gcode_upload" type="file" name="gcode_file" class="fileinput-button" data-url="/ajax/gcodefiles/upload" data-bind="enable: loginState.isUser()">
|
||||
</span>
|
||||
<span class="btn btn-primary fileinput-button span6" data-bind="css: {disabled: !$root.loginState.isUser() || !$root.isSdReady()}" style="margin-bottom: 10px">
|
||||
<i class="icon-upload-alt icon-white"></i>
|
||||
<span>Upload to SD</span>
|
||||
<input id="gcode_upload_sd" type="file" name="gcode_file" class="fileinput-button" data-url="/ajax/gcodefiles/upload" data-bind="enable: loginState.isUser() && isSdReady()">
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="btn btn-primary fileinput-button span12" data-bind="css: {disabled: !$root.loginState.isUser()}" style="margin-bottom: 10px">
|
||||
<i class="icon-upload-alt icon-white"></i>
|
||||
<span>Upload</span>
|
||||
<input id="gcode_upload" type="file" name="gcode_file" class="fileinput-button" data-url="/ajax/gcodefiles/upload" data-bind="enable: loginState.isUser()">
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="gcode_upload_progress" class="progress" style="width: 100%;">
|
||||
<div class="bar" style="width: 0%"></div>
|
||||
</div>
|
||||
|
@ -204,25 +247,27 @@
|
|||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="temp">
|
||||
{% if enableTemperatureGraph %}
|
||||
<div class="row" style="padding-left: 20px">
|
||||
<div id="temperature-graph"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row-fluid" style="margin-bottom: 20px">
|
||||
<div class="form-horizontal span6">
|
||||
<h1>Temperature</h1>
|
||||
|
||||
<label>Current: <strong data-bind="html: tempString"></strong></label>
|
||||
<label title="Current extruder temperature">Current: <strong data-bind="html: tempString"></strong></label>
|
||||
|
||||
<label>Target: <strong data-bind="html: targetTempString"></strong></label>
|
||||
<label title="Target extruder temperature">Target: <strong data-bind="html: targetTempString"></strong></label>
|
||||
|
||||
<div style="display: none;" data-bind="visible: loginState.isUser">
|
||||
<label for="temp_newTemp">New Target</label>
|
||||
<label for="temp_newTemp" title="Sets the new target temperature for the extruder">New Target</label>
|
||||
<div class="input-append">
|
||||
<input type="text" id="temp_newTemp" data-bind="attr: {placeholder: targetTemp}, enable: isOperational() && loginState.isUser()" class="tempInput">
|
||||
<input type="text" class="input-mini text-right" data-bind="value: newTemp, valueUpdate: 'afterkeydown', attr: {placeholder: targetTemp}, enable: isOperational() && loginState.isUser(), event: { keyup: function(d, e) {$root.handleEnter(e, 'temp');} }" class="tempInput">
|
||||
<span class="add-on">°C</span>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button type="submit" class="btn" id="temp_newTemp_set" data-bind="enable: isOperational() && loginState.isUser()">Set</button>
|
||||
<button type="submit" class="btn" data-bind="click: setTemp, enable: isOperational() && loginState.isUser() && newTemp()">Set</button>
|
||||
<button class="btn dropdown-toggle" data-toggle="dropdown" data-bind="enable: isOperational() && loginState.isUser()">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
|
@ -234,27 +279,35 @@
|
|||
<!-- /ko -->
|
||||
<li class="divider"></li>
|
||||
<li>
|
||||
<a href="#" data-bind="click: function() { $root.setTemp(0); }">Off</a>
|
||||
<a href="#" data-bind="click: function() { $root.setTempToZero(); }">Off</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: none;" data-bind="visible: loginState.isUser">
|
||||
<label title="Sets a temperature offset to apply to temperatures set via streamed GCODE, may be positive or negative, will not persist across restarts of OctoPrint">Offset</label>
|
||||
<div class="input-append">
|
||||
<input type="number" min="-50" max="50" class="input-mini text-right" data-bind="value: newTempOffset, valueUpdate: 'afterkeydown', attr: {placeholder: tempOffset}, enable: isOperational() && loginState.isUser(), event: { keyup: function(d, e) {$root.handleEnter(e, 'tempOffset');} }" class="tempInput">
|
||||
<span class="add-on">°C</span>
|
||||
</div>
|
||||
<button type="submit" class="btn" data-bind="click: setTempOffset, enable: newTempOffset() && isOperational() && loginState.isUser()">Set</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-horizontal span6">
|
||||
<h1>Bed Temperature</h1>
|
||||
|
||||
<label>Current: <strong data-bind="html: bedTempString"></strong></label>
|
||||
<label title="Current bed temperature">Current: <strong data-bind="html: bedTempString"></strong></label>
|
||||
|
||||
<label>Target: <strong data-bind="html: bedTargetTempString"></strong></label>
|
||||
<label title="Target bed temperature">Target: <strong data-bind="html: bedTargetTempString"></strong></label>
|
||||
|
||||
<div style="display: none;" data-bind="visible: loginState.isUser">
|
||||
<label for="temp_newBedTemp">New Target</label>
|
||||
<label for="temp_newBedTemp" title="Sets the new target temperature for the bed">New Target</label>
|
||||
<div class="input-append">
|
||||
<input type="text" id="temp_newBedTemp" data-bind="attr: {placeholder: bedTargetTemp}, enable: isOperational() && loginState.isUser()" class="tempInput">
|
||||
<input type="text" class="input-mini text-right" data-bind="value: newBedTemp, valueUpdate: 'afterkeydown', attr: {placeholder: bedTargetTemp}, enable: isOperational() && loginState.isUser(), event: { keyup: function(d, e) {$root.handleEnter(event, 'bedTemp');} }" class="tempInput">
|
||||
<span class="add-on">°C</span>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button type="submit" class="btn" id="temp_newBedTemp_set" data-bind="enable: isOperational() && loginState.isUser()">Set</button>
|
||||
<button type="submit" class="btn" data-bind="click: setBedTemp, enable: isOperational() && loginState.isUser() && newBedTemp()">Set</button>
|
||||
<button class="btn dropdown-toggle" data-toggle="dropdown" data-bind="enable: isOperational() && loginState.isUser()">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
|
@ -266,18 +319,26 @@
|
|||
<!-- /ko -->
|
||||
<li class="divider"></li>
|
||||
<li>
|
||||
<a href="#" data-bind="click: function(){ $root.setBedTemp(0); }">Off</a>
|
||||
<a href="#" data-bind="click: function(){ $root.setBedTempToZero(); }">Off</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: none;" data-bind="visible: loginState.isUser">
|
||||
<label title="Sets a temperature offset to apply to bed temperatures set via streamed GCODE, may be positive or negative, will not persist across restarts of OctoPrint">Offset</label>
|
||||
<div class="input-append">
|
||||
<input type="number" min="-50" max="50" class="input-mini text-right" data-bind="value: newBedTempOffset, valueUpdate: 'afterkeydown', attr: {placeholder: bedTempOffset}, enable: isOperational() && loginState.isUser(), event: { keyup: function(d, e) {$root.handleEnter(e, 'bedTempOffset');} }" class="tempInput">
|
||||
<span class="add-on">°C</span>
|
||||
</div>
|
||||
<button type="submit" class="btn" data-bind="click: setBedTempOffset, enable: newBedTempOffset() && isOperational() && loginState.isUser()">Set</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane" id="control">
|
||||
{% if webcamStream %}
|
||||
<div id="webcam_container">
|
||||
<img id="webcam_image" src="{{ webcamStream }}">
|
||||
<img id="webcam_image" src="{{ webcamStream }}" data-bind="css: { flipH: settings.webcam_flipH(), flipV: settings.webcam_flipV() }">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
@ -356,6 +417,16 @@
|
|||
<button class="btn" data-bind="text: name, enable: $root.isOperational() && $root.loginState.isUser(), click: function() { $root.sendCustomCommand($data) }"></button>
|
||||
</form>
|
||||
</script>
|
||||
<script type="text/html" id="customControls_feedbackCommandTemplate">
|
||||
<form class="form-inline">
|
||||
<button class="btn" data-bind="text: name, enable: $root.isOperational() && $root.loginState.isUser(), click: function() { $root.sendCustomCommand($data) }"></button> <span data-bind="text: output"></span>
|
||||
</form>
|
||||
</script>
|
||||
<script type="text/html" id="customControls_feedbackTemplate">
|
||||
<div>
|
||||
<strong data-bind="text: name"></strong>: <span data-bind="text: output"></span>
|
||||
</div>
|
||||
</script>
|
||||
<script type="text/html" id="customControls_parametricCommandTemplate">
|
||||
<form class="form-inline">
|
||||
<!-- ko foreach: input -->
|
||||
|
@ -368,6 +439,7 @@
|
|||
<script type="text/html" id="customControls_emptyTemplate"><div></div></script>
|
||||
<!-- End of templates for custom controls -->
|
||||
</div>
|
||||
{% if enableGCodeVisualizer %}
|
||||
<div class="tab-pane" id="gcode">
|
||||
<canvas id="canvas" width="572" height="588"></canvas>
|
||||
<div id="slider-vertical"></div>
|
||||
|
@ -455,15 +527,21 @@
|
|||
</div>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="tab-pane" id="term">
|
||||
<pre id="terminal-output" class="pre-scrollable"></pre>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="terminal-autoscroll" data-bind="checked: autoscrollEnabled"> Autoscroll
|
||||
</label>
|
||||
<div data-bind="foreach: filters">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" data-bind="attr: { value: regex }, checked: $parent.activeFilters"> <span data-bind="text: name"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="input-append" style="display: none;" data-bind="visible: loginState.isUser">
|
||||
<input type="text" id="terminal-command" data-bind="enable: isOperational() && loginState.isUser()">
|
||||
<button class="btn" type="button" id="terminal-send" data-bind="enable: isOperational() && loginState.isUser()">Send</button>
|
||||
<input type="text" id="terminal-command" data-bind="value: command, event: { keyup: function(d,e) { handleEnter(e); } }, enable: isOperational() && loginState.isUser()">
|
||||
<button class="btn" type="button" id="terminal-send" data-bind="click: sendCommand, enable: isOperational() && loginState.isUser()">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
{% if enableTimelapse %}
|
||||
|
@ -481,13 +559,19 @@
|
|||
<div id="webcam_timelapse_timedsettings" data-bind="visible: intervalInputEnabled()">
|
||||
<label for="webcam_timelapse_interval">Interval</label>
|
||||
<div class="input-append">
|
||||
<input type="text" class="input-mini" id="webcam_timelapse_interval" data-bind="value: timelapseTimedInterval, enable: isOperational() && !isPrinting() && loginState.isUser()">
|
||||
<input type="text" class="input-mini" id="webcam_timelapse_interval" data-bind="value: timelapseTimedInterval, valueUpdate: 'afterkeydown', enable: isOperational() && !isPrinting() && loginState.isUser()">
|
||||
<span class="add-on">sec</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-bind="visible: loginState.isAdmin">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" data-bind="checked: persist"> Save as default
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="btn" data-bind="click: save, enable: isOperational() && !isPrinting() && loginState.isUser()">Save Settings</button>
|
||||
<button class="btn" data-bind="click: save, enable: saveButtonEnabled">Save config</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -508,7 +592,7 @@
|
|||
<tr data-bind="attr: {title: name}">
|
||||
<td class="timelapse_files_name" data-bind="text: name"></td>
|
||||
<td class="timelapse_files_size" data-bind="text: size"></td>
|
||||
<td class="timelapse_files_action"><a href="#" class="icon-trash" data-bind="click: function() { if ($root.loginState.isUser()) { $parent.removeFile(); } else { return; } }, css: {disabled: !$root.loginState.isUser()}"></a> | <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> | <a href="#" class="icon-download" data-bind="attr: {href: url}"></a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -529,7 +613,12 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<ul>
|
||||
{% if gitBranch and gitCommit %}
|
||||
<ul class="pull-left muted">
|
||||
<li><small>Branch: {{ gitBranch }}, Commit: {{ gitCommit }}</small></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
<ul class="pull-right">
|
||||
<li><a href="http://octoprint.org"><i class="icon-home"></i> Homepage</a></li>
|
||||
<li><a href="https://github.com/foosel/OctoPrint/"><i class="icon-download"></i> Sourcecode</a></li>
|
||||
<li><a href="https://github.com/foosel/OctoPrint/wiki"><i class="icon-book"></i> Documentation</a></li>
|
||||
|
@ -541,23 +630,46 @@
|
|||
{% include 'settings.jinja2' %}
|
||||
{% include 'dialogs.jinja2' %}
|
||||
|
||||
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/modernizr.custom.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/underscore.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/knockout.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/bootstrap/bootstrap.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/bootstrap/bootstrap-modalmanager.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/bootstrap/bootstrap-modal.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery/jquery.ui.core.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery/jquery.ui.widget.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery/jquery.ui.mouse.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery/jquery.ui.slider.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery/jquery.pnotify.min.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery/jquery.flot.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery/jquery.iframe-transport.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery/jquery.fileupload.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/socket.io/socket.io.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/ui.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.min.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/modernizr.custom.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/underscore.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/knockout.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/avltree.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/bootstrap/bootstrap.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/bootstrap/bootstrap-modalmanager.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/bootstrap/bootstrap-modal.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.ui.core.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.ui.widget.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.ui.mouse.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.ui.slider.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.pnotify.min.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.flot.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.iframe-transport.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.fileupload.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/sockjs-0.3.4.min.js') }}"></script>
|
||||
|
||||
<!-- Include OctoPrint files -->
|
||||
<!-- TODO: merge/minimize in the future -->
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/appearance.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/connection.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/control.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/firstrun.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/gcode.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/gcodefiles.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/loginstate.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/navigation.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/printerstate.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/settings.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/temperature.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/terminal.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/timelapse.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/users.js') }}"></script>
|
||||
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/app/dataupdater.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/app/helpers.js') }}"></script>
|
||||
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/app/main.js') }}"></script>
|
||||
<!-- /Include OctoPrint files -->
|
||||
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='gcodeviewer/js/ui.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='gcodeviewer/js/gCodeReader.js') }}"></script>
|
||||
|
|
|
@ -6,17 +6,80 @@
|
|||
<div class="modal-body">
|
||||
<div class="tabbable">
|
||||
<ul class="nav nav-list span4" id="settingsTabs">
|
||||
<li class="active"><a href="#settings_printerParameters" data-toggle="tab">Printer Parameters</a></li>
|
||||
<li><a href="#settings_webcam" data-toggle="tab">Webcam</a></li>
|
||||
<li class="nav-header">Printer</li>
|
||||
<li class="active"><a href="#settings_serialConnection" data-toggle="tab">Serial Connection</a></li>
|
||||
<li><a href="#settings_printerParameters" data-toggle="tab">Printer Parameters</a></li>
|
||||
<li><a href="#settings_temperature" data-toggle="tab">Temperatures</a></li>
|
||||
<li><a href="#settings_terminalFilters" data-toggle="tab">Terminal filters</a></li>
|
||||
<li class="nav-header">Features</li>
|
||||
<li><a href="#settings_features" data-toggle="tab">Features</a></li>
|
||||
<li><a href="#settings_folder" data-toggle="tab">Folder</a></li>
|
||||
<li><a href="#settings_temperature" data-toggle="tab">Temperature</a></li>
|
||||
<li><a href="#settings_webcam" data-toggle="tab">Webcam</a></li>
|
||||
{% if enableAccessControl %}<li><a href="#settings_users" data-toggle="tab">Access Control</a></li>{% endif %}
|
||||
<li><a href="#settings_api" data-toggle="tab">Api</a></li>
|
||||
<li class="nav-header">OctoPrint</li>
|
||||
<li><a href="#settings_folder" data-toggle="tab">Folders</a></li>
|
||||
<li><a href="#settings_appearance" data-toggle="tab">Appearance</a></li>
|
||||
{% if enableAccessControl %}<li><a href="#settings_users" data-toggle="tab">Users</a></li>{% endif %}
|
||||
</ul>
|
||||
|
||||
<div class="tab-content span8">
|
||||
<div class="tab-pane active" id="settings_printerParameters">
|
||||
<div class="tab-pane active" id="settings_serialConnection">
|
||||
<form class="form-horizontal">
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-serialPort">Serial Port</label>
|
||||
<div class="controls">
|
||||
<select id="settings-serialPort" data-bind="options: serial_portOptions, optionsCaption: 'AUTO', value: serial_port"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-baudrate">Baudrate</label>
|
||||
<div class="controls">
|
||||
<select id="settings-baudrate" data-bind="options: serial_baudrateOptions, optionsCaption: 'AUTO', value: serial_baudrate"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" data-bind="checked: serial_autoconnect" id="settings-serialAutoconnect"> Auto-connect to printer on server start
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-movementSpeedE">Communication timeout</label>
|
||||
<div class="controls">
|
||||
<div class="input-append">
|
||||
<input type="number" step="any" min="0" class="input-mini text-right" data-bind="value: serial_timeoutCommunication" id="settings-serialTimeoutCommunication">
|
||||
<span class="add-on">s</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-movementSpeedE">Connection timeout</label>
|
||||
<div class="controls">
|
||||
<div class="input-append">
|
||||
<input type="number" step="any" min="0" class="input-mini text-right" data-bind="value: serial_timeoutConnection" id="settings-serialTimeoutConnection">
|
||||
<span class="add-on">s</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-movementSpeedE">Autodetection timeout</label>
|
||||
<div class="controls">
|
||||
<div class="input-append">
|
||||
<input type="number" step="any" min="0" class="input-mini text-right" data-bind="value: serial_timeoutDetection" id="settings-serialTimeoutDetection">
|
||||
<span class="add-on">s</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" data-bind="checked: serial_log" id="settings-serialLog"> Log communication to serial.log (might negatively impact performance) <span class="label label-important">Warning</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="tab-pane" id="settings_printerParameters">
|
||||
<form class="form-horizontal">
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-movementSpeedX">Movement Speed X Axis</label>
|
||||
|
@ -89,10 +152,29 @@
|
|||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" data-bind="checked: webcam_flipH" id="settings-webcamFlipH"> Flip webcam horizontally
|
||||
</label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" data-bind="checked: webcam_flipV" id="settings-webcamFlipV"> Flip webcam vertically
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="tab-pane" id="settings_features">
|
||||
<form class="form-horizontal">
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" data-bind="checked: feature_temperatureGraph" id="settings-featureTemperatureGraph"> Enable Temperature Graph
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
|
@ -103,7 +185,28 @@
|
|||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" data-bind="checked: feature_waitForStart" id="settings-featureWaitForStart"> Wait for start on connect
|
||||
<input type="checkbox" data-bind="checked: feature_sdSupport" id="settings-featureSdSupport"> Enable SD support
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" data-bind="checked: feature_waitForStart" id="settings-featureWaitForStart"> Wait for <code>start</code> on connect
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" data-bind="checked: feature_alwaysSendChecksum" id="settings-featureAlwaysSendChecksum"> Send a checksum with <strong>every</strong> command <span class="label">Repetier</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" data-bind="checked: feature_swallowOkAfterResend" id="settings-swallowOkAfterResend"> Swallow the first "ok" after a resend response <span class="label">Repetier</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -168,6 +271,32 @@
|
|||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="tab-pane" id="settings_terminalFilters">
|
||||
<form class="form-horizontal">
|
||||
<div class="row-fluid">
|
||||
<div class="span4"><h4>Name</h4></div>
|
||||
<div class="span6"><h4>RegExp <small><a href="https://developer.mozilla.org/en/docs/Web/JavaScript/Guide/Regular_Expressions">?</a></small></h4></div>
|
||||
</div>
|
||||
<div data-bind="foreach: terminalFilters">
|
||||
<div class="row-fluid" style="margin-bottom: 5px">
|
||||
<div class="span4">
|
||||
<input type="text" class="span12" data-bind="value: name">
|
||||
</div>
|
||||
<div class="span6">
|
||||
<input type="text" class="span12" data-bind="value: regex">
|
||||
</div>
|
||||
<div class="span2">
|
||||
<button title="Remove Filter" class="btn btn-danger" data-bind="click: $parent.removeTerminalFilter"><i class="icon-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-fluid">
|
||||
<div class="offset10 span2">
|
||||
<button title="Add Filter" class="btn btn-primary" data-bind="click: addTerminalFilter"><i class="icon-plus"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="tab-pane" id="settings_appearance">
|
||||
<form class="form-horizontal">
|
||||
<div class="control-group">
|
||||
|
@ -185,6 +314,23 @@
|
|||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="tab-pane" id="settings_api">
|
||||
<form class="form-horizontal">
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="settings-apiEnabled" data-bind="checked: api_enabled"> Enable
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-apiKey">Apikey</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-block-level" data-bind="value: api_key" id="settings-apikey">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% if enableAccessControl %}
|
||||
<div class="tab-pane" id="settings_users">
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
# coding=utf-8
|
||||
import logging
|
||||
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
|
||||
from octoprint.settings import settings
|
||||
import octoprint.util as util
|
||||
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import urllib
|
||||
|
@ -14,9 +11,13 @@ import time
|
|||
import subprocess
|
||||
import fnmatch
|
||||
import datetime
|
||||
|
||||
import sys
|
||||
|
||||
import octoprint.util as util
|
||||
|
||||
from octoprint.settings import settings
|
||||
from octoprint.events import eventManager
|
||||
|
||||
def getFinishedTimelapses():
|
||||
files = []
|
||||
basedir = settings().getBaseFolder("timelapse")
|
||||
|
@ -32,10 +33,29 @@ def getFinishedTimelapses():
|
|||
})
|
||||
return files
|
||||
|
||||
validTimelapseTypes = ["off", "timed", "zchange"]
|
||||
|
||||
updateCallbacks = []
|
||||
def registerCallback(callback):
|
||||
if not callback in updateCallbacks:
|
||||
updateCallbacks.append(callback)
|
||||
|
||||
def unregisterCallback(callback):
|
||||
if callback in updateCallbacks:
|
||||
updateCallbacks.remove(callback)
|
||||
|
||||
def notifyCallbacks(timelapse):
|
||||
for callback in updateCallbacks:
|
||||
if timelapse is None:
|
||||
config = None
|
||||
else:
|
||||
config = timelapse.configData()
|
||||
try: callback.sendTimelapseConfig(config)
|
||||
except: pass
|
||||
|
||||
class Timelapse(object):
|
||||
def __init__(self):
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
self._imageNumber = None
|
||||
self._inTimelapse = False
|
||||
self._gcodeFile = None
|
||||
|
@ -47,17 +67,66 @@ class Timelapse(object):
|
|||
self._renderThread = None
|
||||
self._captureMutex = threading.Lock()
|
||||
|
||||
def onPrintjobStarted(self, gcodeFile):
|
||||
self.startTimelapse(gcodeFile)
|
||||
# subscribe events
|
||||
eventManager().subscribe("PrintStarted", self.onPrintStarted)
|
||||
eventManager().subscribe("PrintFailed", self.onPrintDone)
|
||||
eventManager().subscribe("PrintDone", self.onPrintDone)
|
||||
eventManager().subscribe("PrintResumed", self.onPrintResumed)
|
||||
for (event, callback) in self.eventSubscriptions():
|
||||
eventManager().subscribe(event, callback)
|
||||
|
||||
def onPrintjobStopped(self):
|
||||
def unload(self):
|
||||
if self._inTimelapse:
|
||||
self.stopTimelapse(doCreateMovie=False)
|
||||
|
||||
# unsubscribe events
|
||||
eventManager().unsubscribe("PrintStarted", self.onPrintStarted)
|
||||
eventManager().unsubscribe("PrintFailed", self.onPrintDone)
|
||||
eventManager().unsubscribe("PrintDone", self.onPrintDone)
|
||||
eventManager().unsubscribe("PrintResumed", self.onPrintResumed)
|
||||
for (event, callback) in self.eventSubscriptions():
|
||||
eventManager().unsubscribe(event, callback)
|
||||
|
||||
def onPrintStarted(self, event, payload):
|
||||
"""
|
||||
Override this to perform additional actions upon start of a print job.
|
||||
"""
|
||||
self.startTimelapse(payload)
|
||||
|
||||
def onPrintDone(self, event, payload):
|
||||
"""
|
||||
Override this to perform additional actions upon the stop of a print job.
|
||||
"""
|
||||
self.stopTimelapse()
|
||||
|
||||
def onPrintjobProgress(self, oldPos, newPos, percentage):
|
||||
pass
|
||||
def onPrintResumed(self, event, payload):
|
||||
"""
|
||||
Override this to perform additional actions upon the pausing of a print job.
|
||||
"""
|
||||
if not self._inTimelapse:
|
||||
self.startTimelapse(payload)
|
||||
|
||||
def onZChange(self, oldZ, newZ):
|
||||
pass
|
||||
def eventSubscriptions(self):
|
||||
"""
|
||||
Override this method to subscribe to additional events by returning an array of (event, callback) tuples.
|
||||
|
||||
Events that are already subscribed:
|
||||
* PrintStarted - self.onPrintStarted
|
||||
* PrintResumed - self.onPrintResumed
|
||||
* PrintFailed - self.onPrintDone
|
||||
* PrintDone - self.onPrintDone
|
||||
"""
|
||||
return []
|
||||
|
||||
def configData(self):
|
||||
"""
|
||||
Override this method to return the current timelapse configuration data. The data should have the following
|
||||
form:
|
||||
|
||||
type: "<type of timelapse>",
|
||||
options: { <additional options> }
|
||||
"""
|
||||
return None
|
||||
|
||||
def startTimelapse(self, gcodeFile):
|
||||
self._logger.debug("Starting timelapse for %s" % gcodeFile)
|
||||
|
@ -67,11 +136,13 @@ class Timelapse(object):
|
|||
self._inTimelapse = True
|
||||
self._gcodeFile = os.path.basename(gcodeFile)
|
||||
|
||||
def stopTimelapse(self):
|
||||
def stopTimelapse(self, doCreateMovie=True):
|
||||
self._logger.debug("Stopping timelapse")
|
||||
self._renderThread = threading.Thread(target=self._createMovie)
|
||||
self._renderThread.daemon = True
|
||||
self._renderThread.start()
|
||||
|
||||
if doCreateMovie:
|
||||
self._renderThread = threading.Thread(target=self._createMovie)
|
||||
self._renderThread.daemon = True
|
||||
self._renderThread.start()
|
||||
|
||||
self._imageNumber = None
|
||||
self._inTimelapse = False
|
||||
|
@ -85,20 +156,21 @@ class Timelapse(object):
|
|||
filename = os.path.join(self._captureDir, "tmp_%05d.jpg" % (self._imageNumber))
|
||||
self._imageNumber += 1
|
||||
self._logger.debug("Capturing image to %s" % filename)
|
||||
|
||||
captureThread = threading.Thread(target=self._captureWorker, kwargs={"filename": filename})
|
||||
captureThread.daemon = True
|
||||
captureThread.start()
|
||||
|
||||
def _captureWorker(self, filename):
|
||||
eventManager().fire("CaptureStart", filename);
|
||||
urllib.urlretrieve(self._snapshotUrl, filename)
|
||||
self._logger.debug("Image %s captured from %s" % (filename, self._snapshotUrl))
|
||||
eventManager().fire("CaptureDone", filename);
|
||||
|
||||
def _createMovie(self):
|
||||
ffmpeg = settings().get(["webcam", "ffmpeg"])
|
||||
bitrate = settings().get(["webcam", "bitrate"])
|
||||
if ffmpeg is None or bitrate is None:
|
||||
self._logger.warn("Cannot create movie, path to ffmpeg is unset")
|
||||
self._logger.warn("Cannot create movie, path to ffmpeg or desired bitrate is unset")
|
||||
return
|
||||
|
||||
input = os.path.join(self._captureDir, "tmp_%05d.jpg")
|
||||
|
@ -109,19 +181,46 @@ class Timelapse(object):
|
|||
ffmpeg, '-i', input, '-vcodec', 'mpeg2video', '-pix_fmt', 'yuv420p', '-r', '25', '-y', '-b:v', bitrate,
|
||||
'-f', 'vob']
|
||||
|
||||
filters = []
|
||||
|
||||
# flip video if configured
|
||||
if settings().getBoolean(["webcam", "flipH"]):
|
||||
filters.append('hflip')
|
||||
if settings().getBoolean(["webcam", "flipV"]):
|
||||
filters.append('vflip')
|
||||
|
||||
# add watermark if configured
|
||||
watermarkFilter = None
|
||||
if settings().getBoolean(["webcam", "watermark"]):
|
||||
watermark = os.path.join(os.path.dirname(__file__), "static", "img", "watermark.png")
|
||||
if sys.platform == "win32":
|
||||
# Because ffmpeg hiccups on windows' drive letters and backslashes we have to give the watermark
|
||||
# path a special treatment. Yeah, I couldn't believe it either...
|
||||
watermark = watermark.replace("\\", "/").replace(":", "\\\\:")
|
||||
command.extend(['-vf', 'movie=%s [wm]; [in][wm] overlay=10:main_h-overlay_h-10 [out]' % watermark])
|
||||
|
||||
watermarkFilter = "movie=%s [wm]; [%%(inputName)s][wm] overlay=10:main_h-overlay_h-10" % watermark
|
||||
|
||||
filterstring = None
|
||||
if len(filters) > 0:
|
||||
if watermarkFilter is not None:
|
||||
filterstring = "[in] %s [postprocessed]; %s [out]" % (",".join(filters), watermarkFilter % {"inputName": "postprocessed"})
|
||||
else:
|
||||
filterstring = "[in] %s [out]" % ",".join(filters)
|
||||
elif watermarkFilter is not None:
|
||||
filterstring = watermarkFilter % {"inputName": "in"} + " [out]"
|
||||
|
||||
if filterstring is not None:
|
||||
self._logger.debug("Applying videofilter chain: %s" % filterstring)
|
||||
command.extend(["-vf", filterstring])
|
||||
|
||||
# finalize command with output file
|
||||
command.append(output)
|
||||
subprocess.call(command)
|
||||
self._logger.debug("Rendering movie to %s" % output)
|
||||
command.append(output)
|
||||
try:
|
||||
subprocess.check_call(command)
|
||||
eventManager().fire("MovieDone", output)
|
||||
except subprocess.CalledProcessError as (e):
|
||||
self._logger.warn("Could not render movie, got return code %r" % e.returncode)
|
||||
|
||||
def cleanCaptureDir(self):
|
||||
if not os.path.isdir(self._captureDir):
|
||||
|
@ -138,35 +237,53 @@ class ZTimelapse(Timelapse):
|
|||
Timelapse.__init__(self)
|
||||
self._logger.debug("ZTimelapse initialized")
|
||||
|
||||
def onZChange(self, oldZ, newZ):
|
||||
self._logger.debug("Z change detected, capturing image")
|
||||
def eventSubscriptions(self):
|
||||
return [
|
||||
("ZChange", self._onZChange)
|
||||
]
|
||||
|
||||
def configData(self):
|
||||
return {
|
||||
"type": "zchange"
|
||||
}
|
||||
|
||||
def _onZChange(self, event, payload):
|
||||
self.captureImage()
|
||||
|
||||
class TimedTimelapse(Timelapse):
|
||||
def __init__(self, interval=1):
|
||||
Timelapse.__init__(self)
|
||||
|
||||
self._interval = interval
|
||||
if self._interval < 1:
|
||||
self._interval = 1 # force minimum interval of 1s
|
||||
|
||||
self._timerThread = None
|
||||
|
||||
self._logger.debug("TimedTimelapse initialized")
|
||||
|
||||
def interval(self):
|
||||
return self._interval
|
||||
|
||||
def onPrintjobStarted(self, filename):
|
||||
Timelapse.onPrintjobStarted(self, filename)
|
||||
def configData(self):
|
||||
return {
|
||||
"type": "timed",
|
||||
"options": {
|
||||
"interval": self._interval
|
||||
}
|
||||
}
|
||||
|
||||
def onPrintStarted(self, event, payload):
|
||||
Timelapse.onPrintStarted(self, event, payload)
|
||||
if self._timerThread is not None:
|
||||
return
|
||||
|
||||
self._timerThread = threading.Thread(target=self.timerWorker)
|
||||
self._timerThread = threading.Thread(target=self._timerWorker)
|
||||
self._timerThread.daemon = True
|
||||
self._timerThread.start()
|
||||
|
||||
def timerWorker(self):
|
||||
def onPrintDone(self, event, payload):
|
||||
Timelapse.onPrintDone(self, event, payload)
|
||||
self._timerThread = None
|
||||
|
||||
def _timerWorker(self):
|
||||
self._logger.debug("Starting timer for interval based timelapse")
|
||||
while self._inTimelapse:
|
||||
self.captureImage()
|
||||
|
|
|
@ -45,6 +45,8 @@ class UserManager(object):
|
|||
def getAllUsers(self):
|
||||
return []
|
||||
|
||||
def hasBeenCustomized(self):
|
||||
return False
|
||||
|
||||
class HackerspaceUserManager(UserManager):
|
||||
"""A user manager for the Warsaw Hackerspace, uses interal apis."""
|
||||
|
@ -77,17 +79,19 @@ class FilebasedUserManager(UserManager):
|
|||
self._users = {}
|
||||
self._dirty = False
|
||||
|
||||
self._customized = None
|
||||
self._load()
|
||||
|
||||
def _load(self):
|
||||
if os.path.exists(self._userfile) and os.path.isfile(self._userfile):
|
||||
self._customized = True
|
||||
with open(self._userfile, "r") as f:
|
||||
data = yaml.safe_load(f)
|
||||
for name in data.keys():
|
||||
attributes = data[name]
|
||||
self._users[name] = User(name, attributes["password"], attributes["active"], attributes["roles"])
|
||||
else:
|
||||
self._users["admin"] = User("admin", "7557160613d5258f883014a7c3c0428de53040fc152b1791f1cc04a62b428c0c2a9c46ed330cdce9689353ab7a5352ba2b2ceb459b96e9c8ed7d0cb0b2c0c076", True, ["user", "admin"])
|
||||
self._customized = False
|
||||
|
||||
def _save(self, force=False):
|
||||
if not self._dirty and not force:
|
||||
|
@ -189,6 +193,9 @@ class FilebasedUserManager(UserManager):
|
|||
def getAllUsers(self):
|
||||
return map(lambda x: x.asDict(), self._users.values())
|
||||
|
||||
def hasBeenCustomized(self):
|
||||
return self._customized
|
||||
|
||||
##~~ Exceptions
|
||||
|
||||
class UserAlreadyExists(Exception):
|
||||
|
|
|
@ -2,6 +2,5 @@ The code in this sub package mostly originates from the Cura project (https://gi
|
|||
slightly reorganized and adapted. The mapping to the original Cura source is the following:
|
||||
|
||||
* avr_isp.* => Cura.avr_isp.*
|
||||
* comm => Cura.util.machineCom
|
||||
* comm => Cura.util.machineCom (highly modified now)
|
||||
* gcodeInterpreter => Cura.util.gcodeInterpreter
|
||||
* util3d => Cura.util.util3d
|
|
@ -2,9 +2,17 @@
|
|||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
|
||||
import os
|
||||
import traceback
|
||||
import sys
|
||||
import time
|
||||
import re
|
||||
|
||||
from octoprint.settings import settings
|
||||
|
||||
def getFormattedSize(num):
|
||||
"""
|
||||
Taken from http://stackoverflow.com/questions/1094841/reusable-library-to-get-human-readable-version-of-file-size
|
||||
Taken from http://stackoverflow.com/a/1094933/2028598
|
||||
"""
|
||||
for x in ["bytes","KB","MB","GB"]:
|
||||
if num < 1024.0:
|
||||
|
@ -12,9 +20,11 @@ def getFormattedSize(num):
|
|||
num /= 1024.0
|
||||
return "%3.1f%s" % (num, "TB")
|
||||
|
||||
|
||||
def isAllowedFile(filename, extensions):
|
||||
return "." in filename and filename.rsplit(".", 1)[1] in extensions
|
||||
|
||||
|
||||
def getFormattedTimeDelta(d):
|
||||
if d is None:
|
||||
return None
|
||||
|
@ -23,19 +33,108 @@ def getFormattedTimeDelta(d):
|
|||
seconds = d.seconds % 60
|
||||
return "%02d:%02d:%02d" % (hours, minutes, seconds)
|
||||
|
||||
|
||||
def getFormattedDateTime(d):
|
||||
if d is None:
|
||||
return None
|
||||
|
||||
return d.strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
|
||||
def getClass(name):
|
||||
"""
|
||||
Taken from http://stackoverflow.com/a/452981/2028598
|
||||
Taken from http://stackoverflow.com/a/452981/2028598
|
||||
"""
|
||||
parts = name.split(".")
|
||||
module = ".".join(parts[:-1])
|
||||
m = __import__(module)
|
||||
for comp in parts[1:]:
|
||||
m = getattr(m, comp)
|
||||
return m
|
||||
return m
|
||||
|
||||
|
||||
def isDevVersion():
|
||||
gitPath = os.path.abspath(os.path.join(os.path.split(os.path.abspath(__file__))[0], "../../.git"))
|
||||
return os.path.exists(gitPath)
|
||||
|
||||
|
||||
def getExceptionString():
|
||||
locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
|
||||
return "%s: '%s' @ %s:%s:%d" % (str(sys.exc_info()[0].__name__), str(sys.exc_info()[1]), os.path.basename(locationInfo[0]), locationInfo[2], locationInfo[1])
|
||||
|
||||
|
||||
def getGitInfo():
|
||||
gitPath = os.path.abspath(os.path.join(os.path.split(os.path.abspath(__file__))[0], "../../.git"))
|
||||
if not os.path.exists(gitPath):
|
||||
return (None, None)
|
||||
|
||||
headref = None
|
||||
with open(os.path.join(gitPath, "HEAD"), "r") as f:
|
||||
headref = f.readline().strip()
|
||||
|
||||
if headref is None:
|
||||
return (None, None)
|
||||
|
||||
headref = headref[len("ref: "):]
|
||||
branch = headref[headref.rfind("/") + 1:]
|
||||
with open(os.path.join(gitPath, headref)) as f:
|
||||
head = f.readline().strip()
|
||||
|
||||
return (branch, head)
|
||||
|
||||
|
||||
def getNewTimeout(type):
|
||||
now = time.time()
|
||||
|
||||
if type not in ["connection", "detection", "communication"]:
|
||||
return now # timeout immediately for unknown timeout type
|
||||
|
||||
return now + settings().getFloat(["serial", "timeout", type])
|
||||
|
||||
|
||||
def getFreeBytes(path):
|
||||
"""
|
||||
Taken from http://stackoverflow.com/a/2372171/2028598
|
||||
"""
|
||||
if sys.platform == "win32":
|
||||
import ctypes
|
||||
freeBytes = ctypes.c_ulonglong(0)
|
||||
ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(path), None, None, ctypes.pointer(freeBytes))
|
||||
return freeBytes.value
|
||||
else:
|
||||
st = os.statvfs(path)
|
||||
return st.f_bavail * st.f_frsize
|
||||
|
||||
|
||||
def getRemoteAddress(request):
|
||||
forwardedFor = request.headers.get("X-Forwarded-For", None)
|
||||
if forwardedFor is not None:
|
||||
return forwardedFor.split(",")[0]
|
||||
return request.remote_addr
|
||||
|
||||
|
||||
def getDosFilename(input, existingFilenames, extension=None):
|
||||
if input is None:
|
||||
return None
|
||||
|
||||
if extension is None:
|
||||
extension = "gco"
|
||||
|
||||
filename, ext = input.rsplit(".", 1)
|
||||
return findCollisionfreeName(filename, extension, existingFilenames)
|
||||
|
||||
|
||||
def findCollisionfreeName(input, extension, existingFilenames):
|
||||
filename = re.sub(r"\s+", "_", input.lower().translate(None, ".\"/\\[]:;=,"))
|
||||
|
||||
counter = 1
|
||||
power = 1
|
||||
while counter < (10 * power):
|
||||
result = filename[:(6 - power + 1)] + "~" + str(counter) + "." + extension
|
||||
if result not in existingFilenames:
|
||||
return result
|
||||
counter += 1
|
||||
if counter == 10 * power:
|
||||
power += 1
|
||||
|
||||
raise ValueError("Can't create a collision free filename")
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,11 +1,13 @@
|
|||
from __future__ import absolute_import
|
||||
__copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
|
||||
|
||||
import sys
|
||||
import math
|
||||
import re
|
||||
import os
|
||||
import base64
|
||||
import zlib
|
||||
import logging
|
||||
|
||||
from octoprint.util import util3d
|
||||
from octoprint.settings import settings
|
||||
|
||||
preferences = {
|
||||
"extruder_offset_x1": -22.0,
|
||||
|
@ -25,282 +27,208 @@ def getPreference(key, default=None):
|
|||
class AnalysisAborted(Exception):
|
||||
pass
|
||||
|
||||
class gcodePath(object):
|
||||
def __init__(self, newType, pathType, layerThickness, startPoint):
|
||||
self.type = newType
|
||||
self.pathType = pathType
|
||||
self.layerThickness = layerThickness
|
||||
self.list = [startPoint]
|
||||
|
||||
class gcode(object):
|
||||
def __init__(self):
|
||||
self.regMatch = {}
|
||||
self.layerList = []
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
self.layerList = None
|
||||
self.extrusionAmount = 0
|
||||
self.totalMoveTimeMinute = 0
|
||||
self.filename = None
|
||||
self.progressCallback = None
|
||||
self._abort = False
|
||||
self._filamentDiameter = 0
|
||||
|
||||
def load(self, filename):
|
||||
if os.path.isfile(filename):
|
||||
self.filename = filename
|
||||
self._fileSize = os.stat(filename).st_size
|
||||
gcodeFile = open(filename, 'r')
|
||||
self._load(gcodeFile)
|
||||
gcodeFile.close()
|
||||
|
||||
def loadList(self, l):
|
||||
self._load(l)
|
||||
with open(filename, "r") as f:
|
||||
self._load(f)
|
||||
|
||||
def abort(self):
|
||||
self._abort = True
|
||||
|
||||
|
||||
def calculateVolumeCm3(self):
|
||||
radius = self._filamentDiameter / 2
|
||||
return (self.extrusionAmount * (math.pi * radius * radius)) / 1000
|
||||
|
||||
def _load(self, gcodeFile):
|
||||
filePos = 0
|
||||
pos = util3d.Vector3()
|
||||
posOffset = util3d.Vector3()
|
||||
pos = [0.0, 0.0, 0.0]
|
||||
posOffset = [0.0, 0.0, 0.0]
|
||||
currentE = 0.0
|
||||
totalExtrusion = 0.0
|
||||
maxExtrusion = 0.0
|
||||
currentExtruder = 0
|
||||
extrudeAmountMultiply = 1.0
|
||||
totalMoveTimeMinute = 0.0
|
||||
absoluteE = True
|
||||
scale = 1.0
|
||||
posAbs = True
|
||||
posAbsExtruder = True;
|
||||
feedRate = 3600
|
||||
layerThickness = 0.1
|
||||
pathType = 'CUSTOM';
|
||||
startCodeDone = False
|
||||
currentLayer = []
|
||||
unknownGcodes={}
|
||||
unknownMcodes={}
|
||||
currentPath = gcodePath('move', pathType, layerThickness, pos.copy())
|
||||
currentPath.list[0].e = totalExtrusion
|
||||
currentPath.list[0].extrudeAmountMultiply = extrudeAmountMultiply
|
||||
currentLayer.append(currentPath)
|
||||
feedRateXY = settings().getFloat(["printerParameters", "movementSpeed", "x"])
|
||||
|
||||
for line in gcodeFile:
|
||||
if self._abort:
|
||||
raise AnalysisAborted()
|
||||
if type(line) is tuple:
|
||||
line = line[0]
|
||||
if self.progressCallback != None:
|
||||
if isinstance(gcodeFile, (file)):
|
||||
self.progressCallback(float(filePos) / float(self._fileSize))
|
||||
elif isinstance(gcodeFile, (list)):
|
||||
self.progressCallback(float(filePos) / float(len(gcodeFile)))
|
||||
filePos += 1
|
||||
filePos += 1
|
||||
|
||||
try:
|
||||
if self.progressCallback is not None and (filePos % 1000 == 0):
|
||||
if isinstance(gcodeFile, (file)):
|
||||
self.progressCallback(float(gcodeFile.tell()) / float(self._fileSize))
|
||||
elif isinstance(gcodeFile, (list)):
|
||||
self.progressCallback(float(filePos) / float(len(gcodeFile)))
|
||||
except:
|
||||
pass
|
||||
|
||||
#Parse Cura_SF comments
|
||||
if line.startswith(';TYPE:'):
|
||||
pathType = line[6:].strip()
|
||||
if pathType != "CUSTOM":
|
||||
startCodeDone = True
|
||||
|
||||
if ';' in line:
|
||||
#Slic3r GCode comment parser
|
||||
comment = line[line.find(';')+1:].strip()
|
||||
if comment == 'fill':
|
||||
pathType = 'FILL'
|
||||
elif comment == 'perimeter':
|
||||
pathType = 'WALL-INNER'
|
||||
elif comment == 'skirt':
|
||||
pathType = 'SKIRT'
|
||||
if comment.startswith('LAYER:'):
|
||||
self.layerList.append(currentLayer)
|
||||
currentLayer = []
|
||||
if pathType != "CUSTOM":
|
||||
startCodeDone = True
|
||||
if comment.startswith("filament_diameter"):
|
||||
self._filamentDiameter = float(comment.split("=", 1)[1].strip())
|
||||
elif comment.startswith("CURA_PROFILE_STRING"):
|
||||
curaOptions = self._parseCuraProfileString(comment)
|
||||
if "filament_diameter" in curaOptions:
|
||||
try:
|
||||
self._filamentDiameter = float(curaOptions["filament_diameter"])
|
||||
except:
|
||||
self._filamentDiameter = 0.0
|
||||
line = line[0:line.find(';')]
|
||||
T = self.getCodeInt(line, 'T')
|
||||
|
||||
T = getCodeInt(line, 'T')
|
||||
if T is not None:
|
||||
if currentExtruder > 0:
|
||||
posOffset.x -= getPreference('extruder_offset_x%d' % (currentExtruder), 0.0)
|
||||
posOffset.y -= getPreference('extruder_offset_y%d' % (currentExtruder), 0.0)
|
||||
posOffset[0] -= getPreference('extruder_offset_x%d' % (currentExtruder), 0.0)
|
||||
posOffset[1] -= getPreference('extruder_offset_y%d' % (currentExtruder), 0.0)
|
||||
currentExtruder = T
|
||||
if currentExtruder > 0:
|
||||
posOffset.x += getPreference('extruder_offset_x%d' % (currentExtruder), 0.0)
|
||||
posOffset.y += getPreference('extruder_offset_y%d' % (currentExtruder), 0.0)
|
||||
posOffset[0] += getPreference('extruder_offset_x%d' % (currentExtruder), 0.0)
|
||||
posOffset[1] += getPreference('extruder_offset_y%d' % (currentExtruder), 0.0)
|
||||
|
||||
G = self.getCodeInt(line, 'G')
|
||||
G = getCodeInt(line, 'G')
|
||||
if G is not None:
|
||||
if G == 0 or G == 1: #Move
|
||||
x = self.getCodeFloat(line, 'X')
|
||||
y = self.getCodeFloat(line, 'Y')
|
||||
z = self.getCodeFloat(line, 'Z')
|
||||
e = self.getCodeFloat(line, 'E')
|
||||
f = self.getCodeFloat(line, 'F')
|
||||
oldPos = pos.copy()
|
||||
if x is not None:
|
||||
if posAbs:
|
||||
pos.x = x * scale + posOffset.x
|
||||
else:
|
||||
pos.x += x * scale
|
||||
if y is not None:
|
||||
if posAbs:
|
||||
pos.y = y * scale + posOffset.y
|
||||
else:
|
||||
pos.y += y * scale
|
||||
if z is not None:
|
||||
if posAbs:
|
||||
pos.z = z * scale + posOffset.z
|
||||
else:
|
||||
pos.z += z * scale
|
||||
x = getCodeFloat(line, 'X')
|
||||
y = getCodeFloat(line, 'Y')
|
||||
z = getCodeFloat(line, 'Z')
|
||||
e = getCodeFloat(line, 'E')
|
||||
f = getCodeFloat(line, 'F')
|
||||
oldPos = pos
|
||||
pos = pos[:]
|
||||
if posAbs:
|
||||
if x is not None:
|
||||
pos[0] = x * scale + posOffset[0]
|
||||
if y is not None:
|
||||
pos[1] = y * scale + posOffset[1]
|
||||
if z is not None:
|
||||
pos[2] = z * scale + posOffset[2]
|
||||
else:
|
||||
if x is not None:
|
||||
pos[0] += x * scale
|
||||
if y is not None:
|
||||
pos[1] += y * scale
|
||||
if z is not None:
|
||||
pos[2] += z * scale
|
||||
if f is not None:
|
||||
feedRate = f
|
||||
feedRateXY = f
|
||||
if x is not None or y is not None or z is not None:
|
||||
totalMoveTimeMinute += (oldPos - pos).vsize() / feedRate
|
||||
diffX = oldPos[0] - pos[0]
|
||||
diffY = oldPos[1] - pos[1]
|
||||
totalMoveTimeMinute += math.sqrt(diffX * diffX + diffY * diffY) / feedRateXY
|
||||
moveType = 'move'
|
||||
if e is not None:
|
||||
if posAbsExtruder:
|
||||
if e > currentE:
|
||||
moveType = 'extrude'
|
||||
if e < currentE:
|
||||
moveType = 'retract'
|
||||
totalExtrusion += e - currentE
|
||||
currentE = e
|
||||
else:
|
||||
if e > 0:
|
||||
moveType = 'extrude'
|
||||
if e < 0:
|
||||
moveType = 'retract'
|
||||
totalExtrusion += e
|
||||
currentE += e
|
||||
if absoluteE:
|
||||
e -= currentE
|
||||
if e > 0.0:
|
||||
moveType = 'extrude'
|
||||
if e < 0.0:
|
||||
moveType = 'retract'
|
||||
totalExtrusion += e
|
||||
currentE += e
|
||||
if totalExtrusion > maxExtrusion:
|
||||
maxExtrusion = totalExtrusion
|
||||
if moveType == 'move' and oldPos.z != pos.z:
|
||||
if oldPos.z > pos.z and abs(oldPos.z - pos.z) > 5.0 and pos.z < 1.0:
|
||||
oldPos.z = 0.0
|
||||
layerThickness = abs(oldPos.z - pos.z)
|
||||
if currentPath.type != moveType or currentPath.pathType != pathType:
|
||||
currentPath = gcodePath(moveType, pathType, layerThickness, currentPath.list[-1])
|
||||
currentLayer.append(currentPath)
|
||||
newPos = pos.copy()
|
||||
newPos.e = totalExtrusion
|
||||
newPos.extrudeAmountMultiply = extrudeAmountMultiply
|
||||
currentPath.list.append(newPos)
|
||||
else:
|
||||
e = 0.0
|
||||
if moveType == 'move' and oldPos[2] != pos[2]:
|
||||
if oldPos[2] > pos[2] and abs(oldPos[2] - pos[2]) > 5.0 and pos[2] < 1.0:
|
||||
oldPos[2] = 0.0
|
||||
elif G == 4: #Delay
|
||||
S = self.getCodeFloat(line, 'S')
|
||||
S = getCodeFloat(line, 'S')
|
||||
if S is not None:
|
||||
totalMoveTimeMinute += S / 60
|
||||
P = self.getCodeFloat(line, 'P')
|
||||
totalMoveTimeMinute += S / 60.0
|
||||
P = getCodeFloat(line, 'P')
|
||||
if P is not None:
|
||||
totalMoveTimeMinute += P / 60 / 1000
|
||||
totalMoveTimeMinute += P / 60.0 / 1000.0
|
||||
elif G == 20: #Units are inches
|
||||
scale = 25.4
|
||||
elif G == 21: #Units are mm
|
||||
scale = 1.0
|
||||
elif G == 28: #Home
|
||||
x = self.getCodeFloat(line, 'X')
|
||||
y = self.getCodeFloat(line, 'Y')
|
||||
z = self.getCodeFloat(line, 'Z')
|
||||
x = getCodeFloat(line, 'X')
|
||||
y = getCodeFloat(line, 'Y')
|
||||
z = getCodeFloat(line, 'Z')
|
||||
center = [0.0,0.0,0.0]
|
||||
if x is None and y is None and z is None:
|
||||
pos = util3d.Vector3()
|
||||
pos = center
|
||||
else:
|
||||
pos = pos[:]
|
||||
if x is not None:
|
||||
pos.x = 0.0
|
||||
pos[0] = center[0]
|
||||
if y is not None:
|
||||
pos.y = 0.0
|
||||
pos[1] = center[1]
|
||||
if z is not None:
|
||||
pos.z = 0.0
|
||||
pos[2] = center[2]
|
||||
elif G == 90: #Absolute position
|
||||
posAbs = True
|
||||
posAbsExtruder = True
|
||||
elif G == 91: #Relative position
|
||||
posAbs = False
|
||||
posAbsExtruder = False
|
||||
elif G == 92:
|
||||
x = self.getCodeFloat(line, 'X')
|
||||
y = self.getCodeFloat(line, 'Y')
|
||||
z = self.getCodeFloat(line, 'Z')
|
||||
e = self.getCodeFloat(line, 'E')
|
||||
x = getCodeFloat(line, 'X')
|
||||
y = getCodeFloat(line, 'Y')
|
||||
z = getCodeFloat(line, 'Z')
|
||||
e = getCodeFloat(line, 'E')
|
||||
if e is not None:
|
||||
currentE = e
|
||||
if x is not None:
|
||||
posOffset.x = pos.x - x
|
||||
posOffset[0] = pos[0] - x
|
||||
if y is not None:
|
||||
posOffset.y = pos.y - y
|
||||
posOffset[1] = pos[1] - y
|
||||
if z is not None:
|
||||
posOffset.z = pos.z - z
|
||||
else:
|
||||
if G not in unknownGcodes:
|
||||
print "Unknown G code:" + str(G)
|
||||
unknownGcodes[G] = True
|
||||
posOffset[2] = pos[2] - z
|
||||
else:
|
||||
M = self.getCodeInt(line, 'M')
|
||||
M = getCodeInt(line, 'M')
|
||||
if M is not None:
|
||||
if M == 1: #Message with possible wait (ignored)
|
||||
pass
|
||||
elif M == 80: #Enable power supply
|
||||
pass
|
||||
elif M == 81: #Suicide/disable power supply
|
||||
pass
|
||||
elif M == 82: # Use absolute extruder positions
|
||||
posAbsExtruder = True
|
||||
elif M == 83: # Use relative extruder positions
|
||||
posAbsExtruder = False
|
||||
elif M == 84: #Disable step drivers
|
||||
pass
|
||||
elif M == 92: #Set steps per unit
|
||||
pass
|
||||
elif M == 101: #Enable extruder
|
||||
pass
|
||||
elif M == 103: #Disable extruder
|
||||
pass
|
||||
elif M == 104: #Set temperature, no wait
|
||||
pass
|
||||
elif M == 105: #Get temperature
|
||||
pass
|
||||
elif M == 106: #Enable fan
|
||||
pass
|
||||
elif M == 107: #Disable fan
|
||||
pass
|
||||
elif M == 108: #Extruder RPM (these should not be in the final GCode, but they are)
|
||||
pass
|
||||
elif M == 109: #Set temperature, wait
|
||||
pass
|
||||
elif M == 110: #Reset N counter
|
||||
pass
|
||||
elif M == 113: #Extruder PWM (these should not be in the final GCode, but they are)
|
||||
pass
|
||||
elif M == 140: #Set bed temperature
|
||||
pass
|
||||
elif M == 190: #Set bed temperature & wait
|
||||
pass
|
||||
elif M == 221: #Extrude amount multiplier
|
||||
s = self.getCodeFloat(line, 'S')
|
||||
if s != None:
|
||||
extrudeAmountMultiply = s / 100.0
|
||||
else:
|
||||
if M not in unknownMcodes:
|
||||
print "Unknown M code:" + str(M)
|
||||
unknownMcodes[M] = True
|
||||
self.layerList.append(currentLayer)
|
||||
if M == 82: #Absolute E
|
||||
absoluteE = True
|
||||
elif M == 83: #Relative E
|
||||
absoluteE = False
|
||||
if self.progressCallback is not None:
|
||||
self.progressCallback(100.0)
|
||||
self.extrusionAmount = maxExtrusion
|
||||
self.totalMoveTimeMinute = totalMoveTimeMinute
|
||||
|
||||
def getCodeInt(self, line, code):
|
||||
if code not in self.regMatch:
|
||||
self.regMatch[code] = re.compile(code + '([^\s]+)')
|
||||
m = self.regMatch[code].search(line)
|
||||
if m == None:
|
||||
return None
|
||||
try:
|
||||
return int(m.group(1))
|
||||
except:
|
||||
return None
|
||||
def _parseCuraProfileString(self, comment):
|
||||
return {key: value for (key, value) in map(lambda x: x.split("=", 1), zlib.decompress(base64.b64decode(comment[len("CURA_PROFILE_STRING:"):])).split("\b"))}
|
||||
|
||||
def getCodeFloat(self, line, code):
|
||||
if code not in self.regMatch:
|
||||
self.regMatch[code] = re.compile(code + '([^\s]+)')
|
||||
m = self.regMatch[code].search(line)
|
||||
if m == None:
|
||||
return None
|
||||
try:
|
||||
return float(m.group(1))
|
||||
except:
|
||||
return None
|
||||
|
||||
if __name__ == '__main__':
|
||||
for filename in sys.argv[1:]:
|
||||
gcode().load(filename)
|
||||
def getCodeInt(line, code):
|
||||
n = line.find(code) + 1
|
||||
if n < 1:
|
||||
return None
|
||||
m = line.find(' ', n)
|
||||
try:
|
||||
if m < 0:
|
||||
return int(line[n:])
|
||||
return int(line[n:m])
|
||||
except:
|
||||
return None
|
||||
|
||||
def getCodeFloat(line, code):
|
||||
n = line.find(code) + 1
|
||||
if n < 1:
|
||||
return None
|
||||
m = line.find(' ', n)
|
||||
try:
|
||||
if m < 0:
|
||||
return float(line[n:])
|
||||
return float(line[n:m])
|
||||
except:
|
||||
return None
|
||||
|
|
|
@ -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))))
|
||||
|
|
@ -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")
|
||||
|
|
@ -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
|
|
@ -1,8 +1,10 @@
|
|||
flask>=0.9
|
||||
flask==0.9
|
||||
werkzeug==0.8.3
|
||||
tornado==3.0.2
|
||||
sockjs-tornado>=1.0.0
|
||||
PyYAML==3.10
|
||||
Flask-Login==0.2.2
|
||||
Flask-Principal==0.3.5
|
||||
numpy>=1.6.2
|
||||
pyserial>=2.6
|
||||
tornado>=2.4.1
|
||||
tornadio2>=0.0.4
|
||||
PyYAML>=3.10
|
||||
Flask-Login>=0.1.3
|
||||
Flask-Principal>=0.3.5
|
||||
netaddr>=0.7.10
|
||||
|
|
6
run
6
run
|
@ -39,6 +39,10 @@ def main():
|
|||
help="Daemonize/control daemonized OctoPrint instance (only supported under Linux right now)")
|
||||
parser.add_argument("--pid", action="store", type=str, dest="pidfile", default="/tmp/octoprint.pid",
|
||||
help="Pidfile to use for daemonizing, defaults to /tmp/octoprint.pid")
|
||||
|
||||
parser.add_argument("--iknowwhatimdoing", action="store_true", dest="allowRoot",
|
||||
help="Allow OctoPrint to run as user root")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.daemon:
|
||||
|
@ -54,7 +58,7 @@ def main():
|
|||
elif "restart" == args.daemon:
|
||||
daemon.restart()
|
||||
else:
|
||||
octoprint = Server(args.config, args.basedir, args.host, args.port, args.debug)
|
||||
octoprint = Server(args.config, args.basedir, args.host, args.port, args.debug, args.allowRoot)
|
||||
octoprint.run()
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
@ -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())
|
Loading…
Reference in New Issue