WARNING: A lot of changes to the existing API and the event system.
This WILL break existing API clients and probably some event handlers too. I'm sorry for the disruptive changes, but I needed to rectify some decisions before they went too far utilized elsewhere to still be corrected. Basically this change completely removes the old API and switches it (same endpoint) with the new one, that's basically the existing AJAX API that the client uses, but way more RESTful and based on JSON (exception being the file upload). The event system has been revamped to carry more payload data (and in an extensible form as dictionary, to allow for later addition of attributes to single events), with the existing event listeners adjusted to also allow users to make use of this data in their consumers. Documentation has been greatly enhanced for the REST API (and is still being added to), the events will be documented here as well.
This commit is contained in:
parent
049ed723a7
commit
eccc9d6fbd
25 changed files with 720 additions and 471 deletions
BIN
docs/images/octoprint-logo.png
Normal file
BIN
docs/images/octoprint-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
|
|
@ -15,12 +15,72 @@ from octoprint.settings import settings
|
|||
# singleton
|
||||
_instance = None
|
||||
|
||||
|
||||
class Events(object):
|
||||
# application startup
|
||||
STARTUP = "Startup"
|
||||
|
||||
# connect/disconnect to printer
|
||||
CONNECTED = "Connected"
|
||||
DISCONNECTED = "Disconnected"
|
||||
|
||||
# connect/disconnect by client
|
||||
CLIENT_OPENED = "ClientOpened"
|
||||
CLIENT_CLOSED = "ClientClosed"
|
||||
|
||||
# File management
|
||||
UPLOAD = "Upload"
|
||||
FILE_SELECTED = "FileSelected"
|
||||
FILE_DESELECTED = "FileDeselected"
|
||||
UPDATED_FILES = "UpdatedFiles"
|
||||
METADATA_ANALYSIS_STARTED = "MetadataAnalysisStarted"
|
||||
METADATA_ANALYSIS_FINISHED = "MetadataAnalysisFinished"
|
||||
|
||||
# SD Upload
|
||||
TRANSFER_STARTED = "TransferStarted"
|
||||
TRANSFER_DONE = "TransferDone"
|
||||
|
||||
# print job
|
||||
PRINT_STARTED = "PrintStarted"
|
||||
PRINT_DONE = "PrintDone"
|
||||
PRINT_FAILED = "PrintFailed"
|
||||
PRINT_CANCELLED = "PrintCancelled"
|
||||
PRINT_PAUSED = "PrintPaused"
|
||||
PRINT_RESUMED = "PrintResumed"
|
||||
ERROR = "Error"
|
||||
|
||||
# print/gcode events
|
||||
POWER_ON = "PowerOn"
|
||||
POWER_OFF = "PowerOff"
|
||||
HOME = "Home"
|
||||
Z_CHANGE = "ZChange"
|
||||
WAITING = "Waiting"
|
||||
COOLING = "Cooling"
|
||||
ALERT = "Alert"
|
||||
CONVEYOR = "Conveyor"
|
||||
EJECT = "Eject"
|
||||
E_STOP = "EStop"
|
||||
|
||||
# Timelapse
|
||||
CAPTURE_START = "CaptureStart"
|
||||
CAPTURE_DONE = "CaptureDone"
|
||||
MOVIE_RENDERING = "MovieRendering"
|
||||
MOVIE_DONE = "MovieDone"
|
||||
MOVIE_FAILED = "MovieFailed"
|
||||
|
||||
# Slicing
|
||||
SLICING_STARTED = "SlicingStarted"
|
||||
SLICING_DONE = "SlicingDone"
|
||||
SLICING_FAILED = "SlicingFailed"
|
||||
|
||||
|
||||
def eventManager():
|
||||
global _instance
|
||||
if _instance is None:
|
||||
_instance = EventManager()
|
||||
return _instance
|
||||
|
||||
|
||||
class EventManager(object):
|
||||
"""
|
||||
Handles receiving events and dispatching them to subscribers
|
||||
|
|
@ -97,6 +157,7 @@ class EventManager(object):
|
|||
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.
|
||||
|
|
@ -128,21 +189,19 @@ class GenericEventListener(object):
|
|||
"""
|
||||
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", "MovieRendering", "MovieDone", "MovieFailed", "EStop", "Error",
|
||||
"SlicingStarted", "SlicingDone", "SlicingFailed", "UpdatedFiles"]
|
||||
events = filter(lambda x: not x.startswith("__"), dir(Events))
|
||||
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)
|
||||
|
|
@ -244,6 +303,7 @@ class CommandTrigger(GenericEventListener):
|
|||
|
||||
return command.format(**params)
|
||||
|
||||
|
||||
class SystemCommandTrigger(CommandTrigger):
|
||||
"""
|
||||
Performs configured system commands for configured events.
|
||||
|
|
@ -261,6 +321,7 @@ class SystemCommandTrigger(CommandTrigger):
|
|||
except Exception, ex:
|
||||
self._logger.exception("Command failed")
|
||||
|
||||
|
||||
class GcodeCommandTrigger(CommandTrigger):
|
||||
"""
|
||||
Sends configured GCODE commands to the printer for configured events.
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import octoprint.util as util
|
|||
import octoprint.util.gcodeInterpreter as gcodeInterpreter
|
||||
|
||||
from octoprint.settings import settings
|
||||
from octoprint.events import eventManager
|
||||
from octoprint.events import eventManager, Events
|
||||
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
|
|
@ -121,7 +121,7 @@ class GcodeManager:
|
|||
self._metadata[basename] = metadata
|
||||
self._metadataDirty = True
|
||||
self._saveMetadata()
|
||||
eventManager().fire("MetadataAnalysisFinished", {"filename": basename, "result": analysisResult})
|
||||
eventManager().fire(Events.METADATA_ANALYSIS_FINISHED, {"file": basename, "result": analysisResult})
|
||||
|
||||
def _loadMetadata(self):
|
||||
if os.path.exists(self._metadataFile) and os.path.isfile(self._metadataFile):
|
||||
|
|
@ -192,8 +192,9 @@ class GcodeManager:
|
|||
return self.processGcode(absolutePath, destination, uploadCallback), True
|
||||
else:
|
||||
if curaEnabled and isSTLFileName(filename):
|
||||
self.processStl(absolutePath, destination, uploadCallback)
|
||||
return filename, False
|
||||
return self.processStl(absolutePath, destination, uploadCallback), False
|
||||
else:
|
||||
return filename, False
|
||||
|
||||
def getFutureFileName(self, file):
|
||||
if not file:
|
||||
|
|
@ -216,17 +217,19 @@ class GcodeManager:
|
|||
|
||||
def stlProcessed(stlPath, gcodePath, error=None):
|
||||
if error:
|
||||
eventManager().fire("SlicingFailed", {"stl": self._getBasicFilename(stlPath), "gcode": self._getBasicFilename(gcodePath), "reason": error})
|
||||
eventManager().fire(Events.SLICING_FAILED, {"stl": self._getBasicFilename(stlPath), "gcode": self._getBasicFilename(gcodePath), "reason": error})
|
||||
if os.path.exists(stlPath):
|
||||
os.remove(stlPath)
|
||||
else:
|
||||
slicingStop = time.time()
|
||||
eventManager().fire("SlicingDone", {"stl": self._getBasicFilename(stlPath), "gcode": self._getBasicFilename(gcodePath), "time": "%.2f" % (slicingStop - slicingStart)})
|
||||
eventManager().fire(Events.SLICING_DONE, {"stl": self._getBasicFilename(stlPath), "gcode": self._getBasicFilename(gcodePath), "time": "%.2f" % (slicingStop - slicingStart)})
|
||||
self.processGcode(gcodePath, destination, uploadCallback)
|
||||
|
||||
eventManager().fire("SlicingStarted", {"stl": self._getBasicFilename(absolutePath), "gcode": self._getBasicFilename(gcodePath)})
|
||||
eventManager().fire(Events.SLICING_STARTED, {"stl": self._getBasicFilename(absolutePath), "gcode": self._getBasicFilename(gcodePath)})
|
||||
cura.process_file(config, gcodePath, absolutePath, stlProcessed, [absolutePath, gcodePath])
|
||||
|
||||
return self._getBasicFilename(gcodePath)
|
||||
|
||||
def processGcode(self, absolutePath, destination, uploadCallback=None):
|
||||
if absolutePath is None:
|
||||
return None
|
||||
|
|
@ -242,8 +245,9 @@ class GcodeManager:
|
|||
self._metadataAnalyzer.addFileToQueue(os.path.basename(absolutePath))
|
||||
|
||||
if uploadCallback is not None:
|
||||
uploadCallback(filename, absolutePath, destination)
|
||||
return filename
|
||||
return uploadCallback(filename, absolutePath, destination)
|
||||
else:
|
||||
return filename
|
||||
|
||||
def getFutureFilename(self, file):
|
||||
if not file:
|
||||
|
|
@ -298,6 +302,9 @@ class GcodeManager:
|
|||
|
||||
return secure
|
||||
|
||||
def getAllFilenames(self):
|
||||
return map(lambda x: x["name"], self.getAllFileData())
|
||||
|
||||
def getAllFileData(self):
|
||||
files = []
|
||||
for osFile in os.listdir(self._uploadFolder):
|
||||
|
|
@ -509,6 +516,7 @@ class MetadataAnalyzer:
|
|||
|
||||
try:
|
||||
self._logger.debug("Starting analysis of file %s" % filename)
|
||||
eventManager().fire(Events.METADATA_ANALYSIS_STARTED, {"file": filename})
|
||||
self._gcode = gcodeInterpreter.gcode()
|
||||
self._gcode.progressCallback = self._onParsingProgress
|
||||
self._gcode.load(path)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import octoprint.util.comm as comm
|
|||
import octoprint.util as util
|
||||
|
||||
from octoprint.settings import settings
|
||||
from octoprint.events import eventManager
|
||||
from octoprint.events import eventManager, Events
|
||||
|
||||
from octoprint.filemanager.destinations import FileDestinations
|
||||
|
||||
|
|
@ -163,7 +163,7 @@ class Printer():
|
|||
if self._comm is not None:
|
||||
self._comm.close()
|
||||
self._comm = None
|
||||
eventManager().fire("Disconnected")
|
||||
eventManager().fire(Events.DISCONNECTED)
|
||||
|
||||
def command(self, command):
|
||||
"""
|
||||
|
|
@ -247,7 +247,13 @@ class Printer():
|
|||
# mark print as failure
|
||||
if self._selectedFile is not None:
|
||||
self._gcodeManager.printFailed(self._selectedFile["filename"])
|
||||
eventManager().fire("PrintFailed", self._selectedFile["filename"])
|
||||
payload = {
|
||||
"file": self._selectedFile["filename"],
|
||||
"origin": FileDestinations.LOCAL
|
||||
}
|
||||
if self._selectedFile["sd"]:
|
||||
payload["origin"] = FileDestinations.SDCARD
|
||||
eventManager().fire(Events.PRINT_FAILED, payload)
|
||||
|
||||
#~~ state monitoring
|
||||
|
||||
|
|
@ -357,11 +363,6 @@ 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(),
|
||||
|
|
@ -369,7 +370,7 @@ class Printer():
|
|||
"error": self.isError(),
|
||||
"paused": self.isPaused(),
|
||||
"ready": self.isReady(),
|
||||
"sdReady": sdReady
|
||||
"sdReady": self.isSdReady()
|
||||
}
|
||||
|
||||
def getCurrentData(self):
|
||||
|
|
@ -428,7 +429,7 @@ class Printer():
|
|||
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)
|
||||
eventManager().fire(Events.Z_CHANGE, {"new": newZ, "old": oldZ})
|
||||
|
||||
self._setCurrentZ(newZ)
|
||||
|
||||
|
|
@ -436,7 +437,7 @@ class Printer():
|
|||
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
|
||||
|
||||
def mcSdFiles(self, files):
|
||||
eventManager().fire("UpdatedFiles", {"type": "gcode", "files": files})
|
||||
eventManager().fire(Events.UPDATED_FILES, {"type": "gcode", "files": files})
|
||||
self._sdFilelistAvailable.set()
|
||||
|
||||
def mcFileSelected(self, filename, filesize, sd):
|
||||
|
|
@ -489,8 +490,10 @@ class Printer():
|
|||
self.refreshSdFiles(blocking=True)
|
||||
existingSdFiles = self._comm.getSdFiles()
|
||||
|
||||
self._sdRemoteName = util.getDosFilename(filename, existingSdFiles)
|
||||
self._comm.startFileTransfer(absolutePath, self._sdRemoteName)
|
||||
remoteName = util.getDosFilename(filename, existingSdFiles)
|
||||
self._comm.startFileTransfer(absolutePath, filename, remoteName)
|
||||
|
||||
return remoteName
|
||||
|
||||
def deleteSdFile(self, filename):
|
||||
if not self._comm or not self._comm.isSdReady():
|
||||
|
|
@ -583,6 +586,12 @@ class Printer():
|
|||
def isReady(self):
|
||||
return self.isOperational() and not self._comm.isStreaming()
|
||||
|
||||
def isSdReady(self):
|
||||
if not settings().getBoolean(["feature", "sdSupport"]) or self._comm is None:
|
||||
return False
|
||||
else:
|
||||
return self._comm.isSdReady()
|
||||
|
||||
def isLoading(self):
|
||||
return self._gcodeLoader is not None
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ __author__ = "Gina Häußge <osd@foosel.net>"
|
|||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
|
||||
from sockjs.tornado import SockJSRouter
|
||||
from flask import Flask, render_template, send_from_directory
|
||||
from flask import Flask, render_template, send_from_directory, make_response
|
||||
from flask.ext.login import LoginManager
|
||||
from flask.ext.principal import Principal, Permission, RoleNeed, identity_loaded, UserNeed
|
||||
|
||||
|
|
@ -12,6 +12,7 @@ import logging
|
|||
import logging.config
|
||||
|
||||
SUCCESS = {}
|
||||
NO_CONTENT = ("", 204)
|
||||
|
||||
app = Flask("octoprint")
|
||||
debug = False
|
||||
|
|
@ -164,23 +165,21 @@ class Server():
|
|||
logger.info("Listening on http://%s:%d" % (self._host, self._port))
|
||||
app.debug = self._debug
|
||||
|
||||
from octoprint.server.ajax import ajax
|
||||
from octoprint.server.api import api
|
||||
|
||||
app.register_blueprint(ajax, url_prefix="/ajax")
|
||||
app.register_blueprint(api, url_prefix="/api")
|
||||
|
||||
self._router = SockJSRouter(self._createSocketConnection, "/sockjs")
|
||||
|
||||
self._tornado_app = Application(self._router.urls + [
|
||||
(r"/downloads/timelapse/([^/]*\.mpg)", LargeResponseHandler, {"path": settings().getBaseFolder("timelapse"), "as_attachment": True}),
|
||||
(r"/downloads/gcode/([^/]*\.(gco|gcode))", LargeResponseHandler, {"path": settings().getBaseFolder("uploads"), "as_attachment": True}),
|
||||
(r"/downloads/files/local/([^/]*\.(gco|gcode))", LargeResponseHandler, {"path": settings().getBaseFolder("uploads"), "as_attachment": True}),
|
||||
(r".*", FallbackHandler, {"fallback": WSGIContainer(app.wsgi_app)})
|
||||
])
|
||||
self._server = HTTPServer(self._tornado_app)
|
||||
self._server.listen(self._port, address=self._host)
|
||||
|
||||
eventManager.fire("Startup")
|
||||
eventManager.fire(events.Events.STARTUP)
|
||||
if settings().getBoolean(["serial", "autoconnect"]):
|
||||
(port, baudrate) = settings().get(["serial", "port"]), settings().getInt(["serial", "baudrate"])
|
||||
connectionOptions = getConnectionOptions()
|
||||
|
|
|
|||
|
|
@ -1,195 +0,0 @@
|
|||
# coding=utf-8
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
|
||||
from flask import request, jsonify, make_response, url_for
|
||||
|
||||
import octoprint.gcodefiles as gcodefiles
|
||||
import octoprint.util as util
|
||||
from octoprint.filemanager.destinations import FileDestinations
|
||||
from octoprint.settings import settings, valid_boolean_trues
|
||||
from octoprint.server import printer, gcodeManager, eventManager, restricted_access, SUCCESS
|
||||
from octoprint.server.util import redirectToTornado
|
||||
from octoprint.server.ajax import ajax
|
||||
|
||||
|
||||
#~~ GCODE file handling
|
||||
|
||||
|
||||
@ajax.route("/gcodefiles", methods=["GET"])
|
||||
def readGcodeFiles():
|
||||
files = _getFileList(FileDestinations.LOCAL)
|
||||
files.extend(_getFileList(FileDestinations.SDCARD))
|
||||
return jsonify(files=files, free=util.getFormattedSize(util.getFreeBytes(settings().getBaseFolder("uploads"))))
|
||||
|
||||
|
||||
@ajax.route("/gcodefiles/<string:origin>", methods=["GET"])
|
||||
def readGcodeFilesForTarget(origin):
|
||||
if origin not in [FileDestinations.LOCAL, FileDestinations.SDCARD]:
|
||||
return make_response("Invalid target: %s" % origin, 400)
|
||||
|
||||
return jsonify(files=_getFileList(origin), free=util.getFormattedSize(util.getFreeBytes(settings().getBaseFolder("uploads"))))
|
||||
|
||||
|
||||
def _getFileList(origin):
|
||||
if origin == FileDestinations.SDCARD:
|
||||
sdFileList = printer.getSdFiles()
|
||||
|
||||
files = []
|
||||
if sdFileList is not None:
|
||||
for sdFile in sdFileList:
|
||||
files.append({
|
||||
"name": sdFile,
|
||||
"size": "n/a",
|
||||
"bytes": 0,
|
||||
"date": "n/a",
|
||||
"origin": FileDestinations.SDCARD
|
||||
})
|
||||
else:
|
||||
files = gcodeManager.getAllFileData()
|
||||
return files
|
||||
|
||||
|
||||
@ajax.route("/gcodefiles/<string:target>", methods=["POST"])
|
||||
@restricted_access
|
||||
def uploadGcodeFile(target):
|
||||
if not target in [FileDestinations.LOCAL, FileDestinations.SDCARD]:
|
||||
return make_response("Invalid target: %s" % target, 400)
|
||||
|
||||
if "gcode_file" in request.files.keys():
|
||||
file = request.files["gcode_file"]
|
||||
sd = target == FileDestinations.SDCARD
|
||||
selectAfterUpload = "select" in request.values.keys() and request.values["select"] in valid_boolean_trues
|
||||
printAfterSelect = "print" in request.values.keys() and request.values["print"] in valid_boolean_trues
|
||||
|
||||
# determine current job
|
||||
currentFilename = None
|
||||
currentSd = None
|
||||
currentJob = printer.getCurrentJob()
|
||||
if currentJob is not None and "filename" in currentJob.keys() and "sd" in currentJob.keys():
|
||||
currentFilename = currentJob["filename"]
|
||||
currentSd = currentJob["sd"]
|
||||
|
||||
# determine future filename of file to be uploaded, abort if it can't be uploaded
|
||||
futureFilename = gcodeManager.getFutureFilename(file)
|
||||
if futureFilename is None or (not settings().getBoolean(["cura", "enabled"]) and not gcodefiles.isGcodeFileName(futureFilename)):
|
||||
return make_response("Can not upload file %s, wrong format?" % file.filename, 400)
|
||||
|
||||
# prohibit overwriting currently selected file while it's being printed
|
||||
if futureFilename == currentFilename and sd == currentSd and printer.isPrinting() or printer.isPaused():
|
||||
return make_response("Trying to overwrite file that is currently being printed: %s" % currentFilename, 403)
|
||||
|
||||
filename = None
|
||||
|
||||
def fileProcessingFinished(filename, absFilename, destination):
|
||||
"""
|
||||
Callback for when the file processing (upload, optional slicing, addition to analysis queue) has
|
||||
finished.
|
||||
|
||||
Depending on the file's destination triggers either streaming to SD card or directly calls selectOrPrint.
|
||||
"""
|
||||
sd = destination == FileDestinations.SDCARD
|
||||
if sd:
|
||||
printer.addSdFile(filename, absFilename, selectAndOrPrint)
|
||||
else:
|
||||
selectAndOrPrint(absFilename, destination)
|
||||
|
||||
def selectAndOrPrint(nameToSelect, destination):
|
||||
"""
|
||||
Callback for when the file is ready to be selected and optionally printed. For SD file uploads this only
|
||||
the case after they have finished streaming to the printer, which is why this callback is also used
|
||||
for the corresponding call to addSdFile.
|
||||
|
||||
Selects the just uploaded file if either selectAfterUpload or printAfterSelect are True, or if the
|
||||
exact file is already selected, such reloading it.
|
||||
"""
|
||||
sd = destination == FileDestinations.SDCARD
|
||||
if selectAfterUpload or printAfterSelect or (currentFilename == filename and currentSd == sd):
|
||||
printer.selectFile(nameToSelect, sd, printAfterSelect)
|
||||
|
||||
destination = FileDestinations.SDCARD if sd else FileDestinations.LOCAL
|
||||
filename, done = gcodeManager.addFile(file, destination, fileProcessingFinished)
|
||||
if filename is None:
|
||||
return make_response("Could not upload the file %s" % file.filename, 500)
|
||||
|
||||
eventManager.fire("Upload", filename)
|
||||
return jsonify(files=gcodeManager.getAllFileData(), filename=filename, done=done)
|
||||
else:
|
||||
return make_response("No gcode_file included", 400)
|
||||
|
||||
|
||||
@ajax.route("/gcodefiles/local/<path:filename>", methods=["GET"])
|
||||
def readGcodeFile(filename):
|
||||
return redirectToTornado(request, url_for("index") + "downloads/gcode/" + filename)
|
||||
|
||||
|
||||
@ajax.route("/gcodefiles/<string:target>/<path:filename>", methods=["POST"])
|
||||
@restricted_access
|
||||
def gcodeFileCommand(filename, target):
|
||||
if not target in [FileDestinations.LOCAL, FileDestinations.SDCARD]:
|
||||
return make_response("Invalid target: %s" % target, 400)
|
||||
|
||||
# valid file commands, dict mapping command name to mandatory parameters
|
||||
valid_commands = {
|
||||
"load": []
|
||||
}
|
||||
|
||||
command, data, response = util.getJsonCommandFromRequest(request, valid_commands)
|
||||
if response is not None:
|
||||
return response
|
||||
|
||||
if command == "load":
|
||||
# selects/loads a file
|
||||
printAfterLoading = False
|
||||
if "print" in data.keys() and data["print"]:
|
||||
printAfterLoading = True
|
||||
|
||||
sd = False
|
||||
if target == FileDestinations.SDCARD:
|
||||
filenameToSelect = filename
|
||||
sd = True
|
||||
else:
|
||||
filenameToSelect = gcodeManager.getAbsolutePath(filename)
|
||||
printer.selectFile(filenameToSelect, sd, printAfterLoading)
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
return make_response("Command %s is currently not implemented" % command, 400)
|
||||
|
||||
|
||||
@ajax.route("/gcodefiles/<string:target>/<path:filename>", methods=["DELETE"])
|
||||
@restricted_access
|
||||
def deleteGcodeFile(filename, target):
|
||||
if not target in [FileDestinations.LOCAL, FileDestinations.SDCARD]:
|
||||
return make_response("Invalid target: %s" % target, 400)
|
||||
|
||||
sd = target == FileDestinations.SDCARD
|
||||
|
||||
currentJob = printer.getCurrentJob()
|
||||
currentFilename = None
|
||||
currentSd = None
|
||||
if currentJob is not None and "filename" in currentJob.keys() and "sd" in currentJob.keys():
|
||||
currentFilename = currentJob["filename"]
|
||||
currentSd = currentJob["sd"]
|
||||
|
||||
if currentFilename is not None and filename == currentFilename and not (printer.isPrinting() or printer.isPaused()):
|
||||
printer.unselectFile()
|
||||
|
||||
if not (currentFilename == filename and currentSd == sd and (printer.isPrinting() or printer.isPaused())):
|
||||
if sd:
|
||||
printer.deleteSdFile(filename)
|
||||
else:
|
||||
gcodeManager.removeFile(filename)
|
||||
return readGcodeFiles()
|
||||
|
||||
|
||||
@ajax.route("/gcodefiles/<string:target>/refresh", methods=["POST"])
|
||||
@restricted_access
|
||||
def refreshFiles(target):
|
||||
if not target in [FileDestinations.LOCAL, FileDestinations.SDCARD]:
|
||||
return make_response("Invalid target: %s" % target, 400)
|
||||
|
||||
if target == FileDestinations.SDCARD:
|
||||
printer.updateSdFiles()
|
||||
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
# coding=utf-8
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
|
||||
import logging
|
||||
|
||||
from flask import Blueprint, request, jsonify, abort
|
||||
|
||||
from octoprint.server import printer, gcodeManager, SUCCESS
|
||||
from octoprint.server.util import api_access
|
||||
from octoprint.settings import valid_boolean_trues
|
||||
from octoprint.filemanager.destinations import FileDestinations
|
||||
import octoprint.gcodefiles as gcodefiles
|
||||
|
||||
api = Blueprint("api", __name__)
|
||||
|
||||
|
||||
|
||||
@api.route("/load", methods=["POST"])
|
||||
@api_access
|
||||
def apiLoad():
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if not "file" in request.files.keys():
|
||||
abort(400)
|
||||
|
||||
# Perform an upload
|
||||
f = request.files["file"]
|
||||
if not gcodefiles.isGcodeFileName(f.filename):
|
||||
abort(400)
|
||||
|
||||
destination = FileDestinations.LOCAL
|
||||
filename, done = gcodeManager.addFile(f, destination)
|
||||
if filename is None:
|
||||
logger.warn("Upload via API failed")
|
||||
abort(500)
|
||||
|
||||
# Immediately perform a file select and possibly print too
|
||||
printAfterSelect = False
|
||||
if "print" in request.values.keys() and request.values["print"] in valid_boolean_trues:
|
||||
printAfterSelect = True
|
||||
filepath = gcodeManager.getAbsolutePath(filename)
|
||||
if filepath is not None:
|
||||
printer.selectFile(filepath, False, printAfterSelect)
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
@api.route("/state", methods=["GET"])
|
||||
@api_access
|
||||
def apiPrinterState():
|
||||
currentData = printer.getCurrentData()
|
||||
currentData.update({
|
||||
"temperatures": printer.getCurrentTemperatures()
|
||||
})
|
||||
return jsonify(currentData)
|
||||
|
||||
|
|
@ -12,24 +12,24 @@ from flask.ext.principal import Identity, identity_changed, AnonymousIdentity
|
|||
|
||||
import octoprint.util as util
|
||||
import octoprint.users
|
||||
from octoprint.server import restricted_access, SUCCESS, admin_permission, loginManager, principals
|
||||
from octoprint.server import printer, restricted_access, SUCCESS, admin_permission, loginManager, principals
|
||||
from octoprint.settings import settings as s, valid_boolean_trues
|
||||
|
||||
#~~ init ajax blueprint, including sub modules
|
||||
#~~ init api blueprint, including sub modules
|
||||
|
||||
ajax = Blueprint("ajax", __name__)
|
||||
api = Blueprint("api", __name__)
|
||||
|
||||
from . import control as ajax_control
|
||||
from . import gcodefiles as ajax_gcodefiles
|
||||
from . import settings as ajax_settings
|
||||
from . import timelapse as ajax_timelapse
|
||||
from . import users as ajax_users
|
||||
from . import control as api_control
|
||||
from . import files as api_files
|
||||
from . import settings as api_settings
|
||||
from . import timelapse as api_timelapse
|
||||
from . import users as api_users
|
||||
|
||||
|
||||
#~~ first run setup
|
||||
|
||||
|
||||
@ajax.route("/setup", methods=["POST"])
|
||||
@api.route("/setup", methods=["POST"])
|
||||
def firstRunSetup():
|
||||
if not s().getBoolean(["server", "firstRun"]):
|
||||
abort(403)
|
||||
|
|
@ -52,11 +52,23 @@ def firstRunSetup():
|
|||
s().save()
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
#~~ system state
|
||||
|
||||
|
||||
@api.route("/state", methods=["GET"])
|
||||
@restricted_access
|
||||
def apiPrinterState():
|
||||
currentData = printer.getCurrentData()
|
||||
currentData.update({
|
||||
"temperatures": printer.getCurrentTemperatures()
|
||||
})
|
||||
return jsonify(currentData)
|
||||
|
||||
|
||||
#~~ system control
|
||||
|
||||
|
||||
@ajax.route("/system", methods=["POST"])
|
||||
@api.route("/system", methods=["POST"])
|
||||
@restricted_access
|
||||
@admin_permission.require(403)
|
||||
def performSystemAction():
|
||||
|
|
@ -81,7 +93,7 @@ def performSystemAction():
|
|||
#~~ Login/user handling
|
||||
|
||||
|
||||
@ajax.route("/login", methods=["POST"])
|
||||
@api.route("/login", methods=["POST"])
|
||||
def login():
|
||||
if octoprint.server.userManager is not None and "user" in request.values.keys() and "pass" in request.values.keys():
|
||||
username = request.values["user"]
|
||||
|
|
@ -127,7 +139,7 @@ def login():
|
|||
return jsonify(SUCCESS)
|
||||
|
||||
|
||||
@ajax.route("/logout", methods=["POST"])
|
||||
@api.route("/logout", methods=["POST"])
|
||||
@restricted_access
|
||||
def logout():
|
||||
# Remove session keys set by Flask-Principal
|
||||
|
|
@ -6,15 +6,16 @@ from flask import request, jsonify, make_response
|
|||
|
||||
from octoprint.settings import settings
|
||||
from octoprint.printer import getConnectionOptions
|
||||
from octoprint.server import printer, restricted_access, SUCCESS
|
||||
from octoprint.server.ajax import ajax
|
||||
from octoprint.server import printer, restricted_access, NO_CONTENT
|
||||
from octoprint.server.api import api
|
||||
import octoprint.util as util
|
||||
|
||||
|
||||
#~~ Printer control
|
||||
|
||||
|
||||
@ajax.route("/control/connection", methods=["GET"])
|
||||
def connectionOptions():
|
||||
@api.route("/control/connection", methods=["GET"])
|
||||
def connectionState():
|
||||
state, port, baudrate = printer.getCurrentConnection()
|
||||
current = {
|
||||
"state": state,
|
||||
|
|
@ -24,7 +25,7 @@ def connectionOptions():
|
|||
return jsonify({"current": current, "options": getConnectionOptions()})
|
||||
|
||||
|
||||
@ajax.route("/control/connection", methods=["POST"])
|
||||
@api.route("/control/connection", methods=["POST"])
|
||||
@restricted_access
|
||||
def connectionCommand():
|
||||
valid_commands = {
|
||||
|
|
@ -52,20 +53,22 @@ def connectionCommand():
|
|||
if "save" in data.keys() and data["save"]:
|
||||
settings().set(["serial", "port"], port)
|
||||
settings().setInt(["serial", "baudrate"], baudrate)
|
||||
settings().setBoolean(["serial", "autoconnect"], data["autoconnect"])
|
||||
if "autoconnect" in data.keys():
|
||||
settings().setBoolean(["serial", "autoconnect"], data["autoconnect"])
|
||||
settings().save()
|
||||
printer.connect(port=port, baudrate=baudrate)
|
||||
elif command == "disconnect":
|
||||
printer.disconnect()
|
||||
|
||||
return jsonify(SUCCESS)
|
||||
return NO_CONTENT
|
||||
|
||||
|
||||
@ajax.route("/control/printer/command", methods=["POST"])
|
||||
@api.route("/control/printer/command", methods=["POST"])
|
||||
@restricted_access
|
||||
def printerCommand():
|
||||
# TODO: document me
|
||||
if not printer.isOperational():
|
||||
return make_response("Printer is not operational", 403)
|
||||
return make_response("Printer is not operational", 409)
|
||||
|
||||
if not "application/json" in request.headers["Content-Type"]:
|
||||
return make_response("Expected content type JSON", 400)
|
||||
|
|
@ -88,17 +91,18 @@ def printerCommand():
|
|||
|
||||
printer.commands(commandsToSend)
|
||||
|
||||
return jsonify(SUCCESS)
|
||||
return NO_CONTENT
|
||||
|
||||
|
||||
@ajax.route("/control/job", methods=["POST"])
|
||||
@api.route("/control/job", methods=["POST"])
|
||||
@restricted_access
|
||||
def controlJob():
|
||||
if not printer.isOperational():
|
||||
return make_response("Printer is not operational", 403)
|
||||
return make_response("Printer is not operational", 409)
|
||||
|
||||
valid_commands = {
|
||||
"start": [],
|
||||
"restart": [],
|
||||
"pause": [],
|
||||
"cancel": []
|
||||
}
|
||||
|
|
@ -107,20 +111,32 @@ def controlJob():
|
|||
if response is not None:
|
||||
return response
|
||||
|
||||
activePrintjob = printer.isPrinting() or printer.isPaused()
|
||||
|
||||
if command == "start":
|
||||
if activePrintjob:
|
||||
return make_response("Printer already has an active print job, did you mean 'restart'?", 409)
|
||||
printer.startPrint()
|
||||
elif command == "restart":
|
||||
if not printer.isPaused():
|
||||
return make_response("Printer does not have an active print job or is not paused", 409)
|
||||
printer.startPrint()
|
||||
elif command == "pause":
|
||||
if not activePrintjob:
|
||||
return make_response("Printer is neither printing nor paused, 'pause' command cannot be performed", 409)
|
||||
printer.togglePausePrint()
|
||||
elif command == "cancel":
|
||||
if not activePrintjob:
|
||||
return make_response("Printer is neither printing nor paused, 'cancel' command cannot be performed", 409)
|
||||
printer.cancelPrint()
|
||||
return jsonify(SUCCESS)
|
||||
return NO_CONTENT
|
||||
|
||||
|
||||
@ajax.route("/control/printer/hotend", methods=["POST"])
|
||||
@api.route("/control/printer/heater", methods=["POST"])
|
||||
@restricted_access
|
||||
def controlPrinterHotend():
|
||||
if not printer.isOperational():
|
||||
return make_response("Printer is not operational", 403)
|
||||
return make_response("Printer is not operational", 409)
|
||||
|
||||
valid_commands = {
|
||||
"temp": ["temps"],
|
||||
|
|
@ -175,15 +191,15 @@ def controlPrinterHotend():
|
|||
elif "bed" in validated_values:
|
||||
printer.setTemperatureOffset(None, validated_values["bed"])
|
||||
|
||||
return jsonify(SUCCESS)
|
||||
return NO_CONTENT
|
||||
|
||||
|
||||
@ajax.route("/control/printer/printhead", methods=["POST"])
|
||||
@api.route("/control/printer/printhead", methods=["POST"])
|
||||
@restricted_access
|
||||
def controlPrinterPrinthead():
|
||||
if not printer.isOperational() or printer.isPrinting():
|
||||
# do not jog when a print job is running or we don't have a connection
|
||||
return make_response("Printer is not operational or currently printing", 403)
|
||||
return make_response("Printer is not operational or currently printing", 409)
|
||||
|
||||
valid_commands = {
|
||||
"jog": [],
|
||||
|
|
@ -225,15 +241,15 @@ def controlPrinterPrinthead():
|
|||
# TODO make this a generic method call (printer.home(axis, ...)) to get rid of gcode here
|
||||
printer.commands(["G91", "G28 %s" % " ".join(map(lambda x: "%s0" % x.upper(), validated_values)), "G90"])
|
||||
|
||||
return jsonify(SUCCESS)
|
||||
return NO_CONTENT
|
||||
|
||||
|
||||
@ajax.route("/control/printer/feeder", methods=["POST"])
|
||||
@api.route("/control/printer/feeder", methods=["POST"])
|
||||
@restricted_access
|
||||
def controlPrinterFeeder():
|
||||
if not printer.isOperational() or printer.isPrinting():
|
||||
# do not jog when a print job is running or we don't have a connection
|
||||
return make_response("Printer is not operational or currently printing", 403)
|
||||
return make_response("Printer is not operational or currently printing", 409)
|
||||
|
||||
valid_commands = {
|
||||
"extrude": ["amount"]
|
||||
|
|
@ -252,20 +268,23 @@ def controlPrinterFeeder():
|
|||
# TODO make this a generic method call (printer.extruder([hotend,] amount)) to get rid of gcode here
|
||||
printer.commands(["G91", "G1 E%s F%d" % (data["amount"], extrusionSpeed), "G90"])
|
||||
|
||||
return jsonify(SUCCESS)
|
||||
return NO_CONTENT
|
||||
|
||||
|
||||
@ajax.route("/control/custom", methods=["GET"])
|
||||
@api.route("/control/custom", methods=["GET"])
|
||||
def getCustomControls():
|
||||
# TODO: document me
|
||||
customControls = settings().get(["controls"])
|
||||
return jsonify(controls=customControls)
|
||||
|
||||
|
||||
@ajax.route("/control/sd", methods=["POST"])
|
||||
@api.route("/control/printer/sd", methods=["POST"])
|
||||
@restricted_access
|
||||
def sdCommand():
|
||||
if not settings().getBoolean(["feature", "sdSupport"]) or not printer.isOperational() or printer.isPrinting():
|
||||
return make_response("SD support is disabled", 403)
|
||||
if not settings().getBoolean(["feature", "sdSupport"]):
|
||||
return make_response("SD support is disabled", 404)
|
||||
|
||||
if not printer.isOperational() or printer.isPrinting() or printer.isPaused():
|
||||
return make_response("Printer is not operational or currently busy", 409)
|
||||
|
||||
valid_commands = {
|
||||
"init": [],
|
||||
|
|
@ -283,6 +302,12 @@ def sdCommand():
|
|||
elif command == "release":
|
||||
printer.releaseSdCard()
|
||||
|
||||
return jsonify(SUCCESS)
|
||||
return NO_CONTENT
|
||||
|
||||
@api.route("/control/printer/sd", methods=["GET"])
|
||||
def sdState():
|
||||
if not settings().getBoolean(["feature", "sdSupport"]):
|
||||
return make_response("SD support is disabled", 404)
|
||||
|
||||
return jsonify(ready=printer.isSdReady())
|
||||
|
||||
275
src/octoprint/server/api/files.py
Normal file
275
src/octoprint/server/api/files.py
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
# coding=utf-8
|
||||
from octoprint.events import Events
|
||||
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
|
||||
from flask import request, jsonify, make_response, url_for
|
||||
|
||||
import octoprint.gcodefiles as gcodefiles
|
||||
import octoprint.util as util
|
||||
from octoprint.filemanager.destinations import FileDestinations
|
||||
from octoprint.settings import settings, valid_boolean_trues
|
||||
from octoprint.server import printer, gcodeManager, eventManager, restricted_access, NO_CONTENT
|
||||
from octoprint.server.util import redirectToTornado, urlForDownload
|
||||
from octoprint.server.api import api
|
||||
|
||||
|
||||
#~~ GCODE file handling
|
||||
|
||||
|
||||
@api.route("/files", methods=["GET"])
|
||||
def readGcodeFiles():
|
||||
files = _getFileList(FileDestinations.LOCAL)
|
||||
files.extend(_getFileList(FileDestinations.SDCARD))
|
||||
return jsonify(files=files, free=util.getFormattedSize(util.getFreeBytes(settings().getBaseFolder("uploads"))))
|
||||
|
||||
|
||||
@api.route("/files/<string:origin>", methods=["GET"])
|
||||
def readGcodeFilesForOrigin(origin):
|
||||
if origin not in [FileDestinations.LOCAL, FileDestinations.SDCARD]:
|
||||
return make_response("Unknown origin: %s" % origin, 404)
|
||||
|
||||
files = _getFileList(origin)
|
||||
|
||||
if origin == FileDestinations.LOCAL:
|
||||
return jsonify(files=files, free=util.getFormattedSize(util.getFreeBytes(settings().getBaseFolder("uploads"))))
|
||||
else:
|
||||
return jsonify(files=files)
|
||||
|
||||
|
||||
def _getFileDetails(origin, filename):
|
||||
files = _getFileList(origin)
|
||||
for file in files:
|
||||
if file["name"] == filename:
|
||||
return file
|
||||
return None
|
||||
|
||||
|
||||
def _getFileList(origin):
|
||||
if origin == FileDestinations.SDCARD:
|
||||
sdFileList = printer.getSdFiles()
|
||||
|
||||
files = []
|
||||
if sdFileList is not None:
|
||||
for sdFile in sdFileList:
|
||||
files.append({
|
||||
"name": sdFile,
|
||||
"origin": FileDestinations.SDCARD,
|
||||
"refs": {
|
||||
"resource": url_for(".readGcodeFile", target=FileDestinations.SDCARD, filename=sdFile, _external=True)
|
||||
}
|
||||
})
|
||||
else:
|
||||
files = gcodeManager.getAllFileData()
|
||||
for file in files:
|
||||
file.update({
|
||||
"origin": FileDestinations.LOCAL,
|
||||
"refs": {
|
||||
"resource": url_for(".readGcodeFile", target=FileDestinations.LOCAL, filename=file["name"], _external=True),
|
||||
"download": urlForDownload(FileDestinations.LOCAL, file["name"])
|
||||
}
|
||||
})
|
||||
return files
|
||||
|
||||
|
||||
def _verifyFileExists(origin, filename):
|
||||
if origin == FileDestinations.SDCARD:
|
||||
availableFiles = printer.getSdFiles()
|
||||
else:
|
||||
availableFiles = gcodeManager.getAllFilenames()
|
||||
|
||||
return filename in availableFiles
|
||||
|
||||
|
||||
@api.route("/files/<string:target>", methods=["POST"])
|
||||
@restricted_access
|
||||
def uploadGcodeFile(target):
|
||||
if not target in [FileDestinations.LOCAL, FileDestinations.SDCARD]:
|
||||
return make_response("Unknown target: %s" % target, 404)
|
||||
|
||||
if not "file" in request.files.keys():
|
||||
return make_response("No file included", 400)
|
||||
|
||||
if target == FileDestinations.SDCARD and not settings().getBoolean(["feature", "sdSupport"]):
|
||||
return make_response("SD card support is disabled", 404)
|
||||
|
||||
file = request.files["file"]
|
||||
sd = target == FileDestinations.SDCARD
|
||||
selectAfterUpload = "select" in request.values.keys() and request.values["select"] in valid_boolean_trues
|
||||
printAfterSelect = "print" in request.values.keys() and request.values["print"] in valid_boolean_trues
|
||||
|
||||
if sd:
|
||||
# validate that all preconditions for SD upload are met before attempting it
|
||||
if not (printer.isOperational() and not (printer.isPrinting() or printer.isPaused())):
|
||||
return make_response("Can not upload to SD card, printer is either not operational or already busy", 409)
|
||||
if not printer.isSdReady():
|
||||
return make_response("Can not upload to SD card, not yet initialized", 409)
|
||||
|
||||
# determine current job
|
||||
currentFilename = None
|
||||
currentSd = None
|
||||
currentJob = printer.getCurrentJob()
|
||||
if currentJob is not None and "filename" in currentJob.keys() and "sd" in currentJob.keys():
|
||||
currentFilename = currentJob["filename"]
|
||||
currentSd = currentJob["sd"]
|
||||
|
||||
# determine future filename of file to be uploaded, abort if it can't be uploaded
|
||||
futureFilename = gcodeManager.getFutureFilename(file)
|
||||
if futureFilename is None or (not settings().getBoolean(["cura", "enabled"]) and not gcodefiles.isGcodeFileName(futureFilename)):
|
||||
return make_response("Can not upload file %s, wrong format?" % file.filename, 415)
|
||||
|
||||
# prohibit overwriting currently selected file while it's being printed
|
||||
if futureFilename == currentFilename and sd == currentSd and printer.isPrinting() or printer.isPaused():
|
||||
return make_response("Trying to overwrite file that is currently being printed: %s" % currentFilename, 409)
|
||||
|
||||
filename = None
|
||||
|
||||
def fileProcessingFinished(filename, absFilename, destination):
|
||||
"""
|
||||
Callback for when the file processing (upload, optional slicing, addition to analysis queue) has
|
||||
finished.
|
||||
|
||||
Depending on the file's destination triggers either streaming to SD card or directly calls selectAndOrPrint.
|
||||
"""
|
||||
if destination == FileDestinations.SDCARD:
|
||||
return filename, printer.addSdFile(filename, absFilename, selectAndOrPrint)
|
||||
else:
|
||||
selectAndOrPrint(absFilename, destination)
|
||||
return filename
|
||||
|
||||
def selectAndOrPrint(nameToSelect, destination):
|
||||
"""
|
||||
Callback for when the file is ready to be selected and optionally printed. For SD file uploads this is only
|
||||
the case after they have finished streaming to the printer, which is why this callback is also used
|
||||
for the corresponding call to addSdFile.
|
||||
|
||||
Selects the just uploaded file if either selectAfterUpload or printAfterSelect are True, or if the
|
||||
exact file is already selected, such reloading it.
|
||||
"""
|
||||
sd = destination == FileDestinations.SDCARD
|
||||
if selectAfterUpload or printAfterSelect or (currentFilename == filename and currentSd == sd):
|
||||
printer.selectFile(nameToSelect, sd, printAfterSelect)
|
||||
|
||||
destination = FileDestinations.SDCARD if sd else FileDestinations.LOCAL
|
||||
filename, done = gcodeManager.addFile(file, destination, fileProcessingFinished)
|
||||
if filename is None:
|
||||
return make_response("Could not upload the file %s" % file.filename, 500)
|
||||
|
||||
sdFilename = None
|
||||
if isinstance(filename, tuple):
|
||||
filename, sdFilename = filename
|
||||
|
||||
eventManager.fire(Events.UPLOAD, {"file": filename, "target": target})
|
||||
|
||||
files = {}
|
||||
if done:
|
||||
files.update({
|
||||
FileDestinations.LOCAL: {
|
||||
"name": filename,
|
||||
"origin": FileDestinations.LOCAL,
|
||||
"refs": {
|
||||
"resource": url_for(".readGcodeFile", target=FileDestinations.LOCAL, filename=filename, _external=True),
|
||||
"download": urlForDownload(FileDestinations.LOCAL, filename)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if sd and sdFilename:
|
||||
files.update({
|
||||
FileDestinations.SDCARD: {
|
||||
"name": sdFilename,
|
||||
"origin": FileDestinations.SDCARD,
|
||||
"refs": {
|
||||
"resource": url_for(".readGcodeFile", target=FileDestinations.SDCARD, filename=sdFilename, _external=True)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return make_response(jsonify(files=files, done=done), 201)
|
||||
|
||||
|
||||
@api.route("/files/<string:target>/<path:filename>", methods=["GET"])
|
||||
def readGcodeFile(target, filename):
|
||||
if not target in [FileDestinations.LOCAL, FileDestinations.SDCARD]:
|
||||
return make_response("Unknown target: %s" % target, 404)
|
||||
|
||||
file = _getFileDetails(target, filename)
|
||||
if not file:
|
||||
return make_response("File not found on '%s': %s" % (target, filename), 404)
|
||||
|
||||
return jsonify(file)
|
||||
|
||||
|
||||
@api.route("/files/<string:target>/<path:filename>", methods=["POST"])
|
||||
@restricted_access
|
||||
def gcodeFileCommand(filename, target):
|
||||
if not target in [FileDestinations.LOCAL, FileDestinations.SDCARD]:
|
||||
return make_response("Unknown target: %s" % target, 404)
|
||||
|
||||
if not _verifyFileExists(target, filename):
|
||||
return make_response("File not found on '%s': %s" % (target, filename), 404)
|
||||
|
||||
# valid file commands, dict mapping command name to mandatory parameters
|
||||
valid_commands = {
|
||||
"select": []
|
||||
}
|
||||
|
||||
command, data, response = util.getJsonCommandFromRequest(request, valid_commands)
|
||||
if response is not None:
|
||||
return response
|
||||
|
||||
if command == "select":
|
||||
# selects/loads a file
|
||||
printAfterLoading = False
|
||||
if "print" in data.keys() and data["print"]:
|
||||
if not printer.isOperational():
|
||||
return make_response("Printer is not operational, cannot directly start printing", 409)
|
||||
printAfterLoading = True
|
||||
|
||||
sd = False
|
||||
if target == FileDestinations.SDCARD:
|
||||
filenameToSelect = filename
|
||||
sd = True
|
||||
else:
|
||||
filenameToSelect = gcodeManager.getAbsolutePath(filename)
|
||||
printer.selectFile(filenameToSelect, sd, printAfterLoading)
|
||||
|
||||
return NO_CONTENT
|
||||
|
||||
|
||||
@api.route("/files/<string:target>/<path:filename>", methods=["DELETE"])
|
||||
@restricted_access
|
||||
def deleteGcodeFile(filename, target):
|
||||
if not target in [FileDestinations.LOCAL, FileDestinations.SDCARD]:
|
||||
return make_response("Unknown target: %s" % target, 404)
|
||||
|
||||
if not _verifyFileExists(target, filename):
|
||||
return make_response("File not found on '%s': %s" % (target, filename), 404)
|
||||
|
||||
sd = target == FileDestinations.SDCARD
|
||||
|
||||
currentJob = printer.getCurrentJob()
|
||||
currentFilename = None
|
||||
currentSd = None
|
||||
if currentJob is not None and "filename" in currentJob.keys() and "sd" in currentJob.keys():
|
||||
currentFilename = currentJob["filename"]
|
||||
currentSd = currentJob["sd"]
|
||||
|
||||
# prohibit deleting the file that is currently being printed
|
||||
if currentFilename == filename and currentSd == sd and (printer.isPrinting() or printer.isPaused()):
|
||||
make_response("Trying to delete file that is currently being printed: %s" % filename, 409)
|
||||
|
||||
# deselect the file if it's currently selected
|
||||
if currentFilename is not None and filename == currentFilename:
|
||||
printer.unselectFile()
|
||||
|
||||
# delete it
|
||||
if sd:
|
||||
printer.deleteSdFile(filename)
|
||||
else:
|
||||
gcodeManager.removeFile(filename)
|
||||
|
||||
# return an updated list of files
|
||||
return readGcodeFiles()
|
||||
|
||||
|
|
@ -10,13 +10,13 @@ from octoprint.settings import settings
|
|||
from octoprint.printer import getConnectionOptions
|
||||
|
||||
from octoprint.server import restricted_access, admin_permission
|
||||
from octoprint.server.ajax import ajax
|
||||
from octoprint.server.api import api
|
||||
|
||||
|
||||
#~~ settings
|
||||
|
||||
|
||||
@ajax.route("/settings", methods=["GET"])
|
||||
@api.route("/settings", methods=["GET"])
|
||||
def getSettings():
|
||||
s = settings()
|
||||
|
||||
|
|
@ -91,7 +91,7 @@ def getSettings():
|
|||
})
|
||||
|
||||
|
||||
@ajax.route("/settings", methods=["POST"])
|
||||
@api.route("/settings", methods=["POST"])
|
||||
@restricted_access
|
||||
@admin_permission.require(403)
|
||||
def setSettings():
|
||||
|
|
@ -13,43 +13,42 @@ from octoprint.settings import settings, valid_boolean_trues
|
|||
|
||||
from octoprint.server import restricted_access, admin_permission
|
||||
from octoprint.server.util import redirectToTornado
|
||||
from octoprint.server.ajax import ajax
|
||||
from octoprint.server.api import api
|
||||
|
||||
|
||||
#~~ timelapse handling
|
||||
|
||||
|
||||
@ajax.route("/timelapse", methods=["GET"])
|
||||
@api.route("/timelapse", methods=["GET"])
|
||||
def getTimelapseData():
|
||||
timelapse = octoprint.timelapse.current
|
||||
|
||||
type = "off"
|
||||
additionalConfig = {}
|
||||
config = {"type": "off"}
|
||||
if timelapse is not None and isinstance(timelapse, octoprint.timelapse.ZTimelapse):
|
||||
type = "zchange"
|
||||
config["type"] = "zchange"
|
||||
elif timelapse is not None and isinstance(timelapse, octoprint.timelapse.TimedTimelapse):
|
||||
type = "timed"
|
||||
additionalConfig = {
|
||||
config["type"] = "timed"
|
||||
config.update({
|
||||
"interval": timelapse.interval()
|
||||
}
|
||||
})
|
||||
|
||||
files = octoprint.timelapse.getFinishedTimelapses()
|
||||
for file in files:
|
||||
file["url"] = url_for("index") + "downloads/timelapse/" + file["name"]
|
||||
|
||||
return jsonify({
|
||||
"type": type,
|
||||
"config": additionalConfig,
|
||||
"config": config,
|
||||
"files": files
|
||||
})
|
||||
|
||||
|
||||
@ajax.route("/timelapse/<filename>", methods=["GET"])
|
||||
@api.route("/timelapse/<filename>", methods=["GET"])
|
||||
def downloadTimelapse(filename):
|
||||
return redirectToTornado(request, url_for("index") + "downloads/timelapse/" + filename)
|
||||
|
||||
|
||||
@ajax.route("/timelapse/<filename>", methods=["DELETE"])
|
||||
@api.route("/timelapse/<filename>", methods=["DELETE"])
|
||||
@restricted_access
|
||||
def deleteTimelapse(filename):
|
||||
if util.isAllowedFile(filename, {"mpg"}):
|
||||
|
|
@ -59,7 +58,7 @@ def deleteTimelapse(filename):
|
|||
return getTimelapseData()
|
||||
|
||||
|
||||
@ajax.route("/timelapse", methods=["POST"])
|
||||
@api.route("/timelapse", methods=["POST"])
|
||||
@restricted_access
|
||||
def setTimelapseConfig():
|
||||
if "type" in request.values:
|
||||
|
|
@ -8,13 +8,13 @@ from flask.ext.login import current_user
|
|||
import octoprint.users as users
|
||||
|
||||
from octoprint.server import restricted_access, SUCCESS, admin_permission, userManager
|
||||
from octoprint.server.ajax import ajax
|
||||
from octoprint.server.api import api
|
||||
|
||||
|
||||
#~~ user settings
|
||||
|
||||
|
||||
@ajax.route("/users", methods=["GET"])
|
||||
@api.route("/users", methods=["GET"])
|
||||
@restricted_access
|
||||
@admin_permission.require(403)
|
||||
def getUsers():
|
||||
|
|
@ -24,7 +24,7 @@ def getUsers():
|
|||
return jsonify({"users": userManager.getAllUsers()})
|
||||
|
||||
|
||||
@ajax.route("/users", methods=["POST"])
|
||||
@api.route("/users", methods=["POST"])
|
||||
@restricted_access
|
||||
@admin_permission.require(403)
|
||||
def addUser():
|
||||
|
|
@ -49,7 +49,7 @@ def addUser():
|
|||
return getUsers()
|
||||
|
||||
|
||||
@ajax.route("/users/<username>", methods=["GET"])
|
||||
@api.route("/users/<username>", methods=["GET"])
|
||||
@restricted_access
|
||||
def getUser(username):
|
||||
if userManager is None:
|
||||
|
|
@ -65,7 +65,7 @@ def getUser(username):
|
|||
abort(403)
|
||||
|
||||
|
||||
@ajax.route("/users/<username>", methods=["PUT"])
|
||||
@api.route("/users/<username>", methods=["PUT"])
|
||||
@restricted_access
|
||||
@admin_permission.require(403)
|
||||
def updateUser(username):
|
||||
|
|
@ -91,7 +91,7 @@ def updateUser(username):
|
|||
abort(404)
|
||||
|
||||
|
||||
@ajax.route("/users/<username>", methods=["DELETE"])
|
||||
@api.route("/users/<username>", methods=["DELETE"])
|
||||
@restricted_access
|
||||
@admin_permission.require(http_exception=403)
|
||||
def removeUser(username):
|
||||
|
|
@ -105,7 +105,7 @@ def removeUser(username):
|
|||
abort(404)
|
||||
|
||||
|
||||
@ajax.route("/users/<username>/password", methods=["PUT"])
|
||||
@api.route("/users/<username>/password", methods=["PUT"])
|
||||
@restricted_access
|
||||
def changePasswordForUser(username):
|
||||
if userManager is None:
|
||||
|
|
@ -124,7 +124,7 @@ def changePasswordForUser(username):
|
|||
return make_response(("Forbidden", 403, []))
|
||||
|
||||
|
||||
@ajax.route("/users/<username>/apikey", methods=["DELETE"])
|
||||
@api.route("/users/<username>/apikey", methods=["DELETE"])
|
||||
@restricted_access
|
||||
def deleteApikeyForUser(username):
|
||||
if userManager is None:
|
||||
|
|
@ -140,7 +140,7 @@ def deleteApikeyForUser(username):
|
|||
return make_response(("Forbidden", 403, []))
|
||||
|
||||
|
||||
@ajax.route("/users/<username>/apikey", methods=["POST"])
|
||||
@api.route("/users/<username>/apikey", methods=["POST"])
|
||||
@restricted_access
|
||||
def generateApikeyForUser(username):
|
||||
if userManager is None:
|
||||
|
|
@ -19,6 +19,7 @@ from octoprint.settings import settings
|
|||
import octoprint.timelapse
|
||||
import octoprint.server
|
||||
from octoprint.users import ApiUser
|
||||
from octoprint.events import Events
|
||||
|
||||
def restricted_access(func, apiEnabled=True):
|
||||
"""
|
||||
|
|
@ -45,10 +46,8 @@ def restricted_access(func, apiEnabled=True):
|
|||
return make_response("OctoPrint isn't setup yet", 403)
|
||||
|
||||
# if API is globally enabled, enabled for this request and an api key is provided, try to use that
|
||||
if settings().get(["api", "enabled"]) and apiEnabled and "apikey" in request.values.keys():
|
||||
apikey = request.values["apikey"]
|
||||
user = None
|
||||
|
||||
apikey = _getApiKey(request)
|
||||
if settings().get(["api", "enabled"]) and apiEnabled and apikey is not None:
|
||||
if apikey == settings().get(["api", "key"]):
|
||||
# master key was used
|
||||
user = ApiUser()
|
||||
|
|
@ -57,7 +56,7 @@ def restricted_access(func, apiEnabled=True):
|
|||
user = octoprint.server.userManager.findUser(apikey=apikey)
|
||||
|
||||
if user is None:
|
||||
make_response("Invalid API key", 403)
|
||||
make_response("Invalid API key", 401)
|
||||
if login_user(user, remember=False):
|
||||
identity_changed.send(current_app._get_current_object(), identity=Identity(user.get_id()))
|
||||
return func(*args, **kwargs)
|
||||
|
|
@ -72,19 +71,30 @@ def api_access(func):
|
|||
def decorated_view(*args, **kwargs):
|
||||
if not settings().get(["api", "enabled"]):
|
||||
make_response("API disabled", 401)
|
||||
if not "apikey" in request.values.keys():
|
||||
apikey = _getApiKey(request)
|
||||
if apikey is None:
|
||||
make_response("No API key provided", 401)
|
||||
if request.values["apikey"] != settings().get(["api", "key"]):
|
||||
if apikey != settings().get(["api", "key"]):
|
||||
make_response("Invalid API key", 403)
|
||||
return func(*args, **kwargs)
|
||||
return decorated_view
|
||||
|
||||
|
||||
def _getApiKey(request):
|
||||
if "apikey" in request.values:
|
||||
return request.values["apikey"]
|
||||
if "X-Api-Key" in request.headers.keys():
|
||||
return request.headers.get("X-Api-Key")
|
||||
return None
|
||||
|
||||
|
||||
#~~ Printer state
|
||||
|
||||
|
||||
class PrinterStateConnection(SockJSConnection):
|
||||
EVENTS = ["UpdatedFiles", "MetadataAnalysisFinished", "MovieRendering", "MovieDone",
|
||||
"MovieFailed", "SlicingStarted", "SlicingDone", "SlicingFailed"]
|
||||
EVENTS = [Events.UPDATED_FILES, Events.METADATA_ANALYSIS_FINISHED, Events.MOVIE_RENDERING, Events.MOVIE_DONE,
|
||||
Events.MOVIE_FAILED, Events.SLICING_STARTED, Events.SLICING_DONE, Events.SLICING_FAILED,
|
||||
Events.TRANSFER_STARTED, Events.TRANSFER_DONE]
|
||||
|
||||
def __init__(self, printer, gcodeManager, userManager, eventManager, session):
|
||||
SockJSConnection.__init__(self, session)
|
||||
|
|
@ -110,24 +120,25 @@ class PrinterStateConnection(SockJSConnection):
|
|||
return info.ip
|
||||
|
||||
def on_open(self, info):
|
||||
self._logger.info("New connection from client: %s" % self._getRemoteAddress(info))
|
||||
remoteAddress = self._getRemoteAddress(info)
|
||||
self._logger.info("New connection from client: %s" % remoteAddress)
|
||||
self._printer.registerCallback(self)
|
||||
self._gcodeManager.registerCallback(self)
|
||||
octoprint.timelapse.registerCallback(self)
|
||||
|
||||
self._eventManager.fire("ClientOpened")
|
||||
self._eventManager.fire(Events.CLIENT_OPENED, {"remoteAddress": remoteAddress})
|
||||
for event in PrinterStateConnection.EVENTS:
|
||||
self._eventManager.subscribe(event, self._onEvent)
|
||||
|
||||
octoprint.timelapse.notifyCallbacks(octoprint.timelapse.current)
|
||||
|
||||
def on_close(self):
|
||||
self._logger.info("Closed client connection")
|
||||
self._logger.info("Client connection closed")
|
||||
self._printer.unregisterCallback(self)
|
||||
self._gcodeManager.unregisterCallback(self)
|
||||
octoprint.timelapse.unregisterCallback(self)
|
||||
|
||||
self._eventManager.fire("ClientClosed")
|
||||
self._eventManager.fire(Events.CLIENT_CLOSED)
|
||||
for event in PrinterStateConnection.EVENTS:
|
||||
self._eventManager.unsubscribe(event, self._onEvent)
|
||||
|
||||
|
|
@ -314,7 +325,7 @@ class ReverseProxied(object):
|
|||
|
||||
def redirectToTornado(request, target):
|
||||
requestUrl = request.url
|
||||
appBaseUrl = requestUrl[:requestUrl.find(url_for("index") + "/ajax")]
|
||||
appBaseUrl = requestUrl[:requestUrl.find(url_for("index") + "api")]
|
||||
|
||||
redirectUrl = appBaseUrl + target
|
||||
if "?" in requestUrl:
|
||||
|
|
@ -322,3 +333,7 @@ def redirectToTornado(request, target):
|
|||
redirectUrl += fragment
|
||||
return redirect(redirectUrl)
|
||||
|
||||
|
||||
def urlForDownload(origin, filename):
|
||||
return url_for("index", _external=True) + "downloads/files/" + origin + "/" + filename
|
||||
|
||||
|
|
|
|||
|
|
@ -133,6 +133,16 @@ function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewM
|
|||
gcodeUploadProgressBar.css("width", "0%");
|
||||
gcodeUploadProgressBar.text("");
|
||||
$.pnotify({title: "Slicing failed", text: "Could not slice " + payload.stl + " to " + payload.gcode + ": " + payload.reason, type: "error"});
|
||||
} else if (type == "TransferStarted") {
|
||||
gcodeUploadProgress.addClass("progress-striped").addClass("active");
|
||||
gcodeUploadProgressBar.css("width", "100%");
|
||||
gcodeUploadProgressBar.text("Streaming ...");
|
||||
} else if (type == "TransferDone") {
|
||||
gcodeUploadProgress.removeClass("progress-striped").removeClass("active");
|
||||
gcodeUploadProgressBar.css("width", "0%");
|
||||
gcodeUploadProgressBar.text("");
|
||||
$.pnotify({title: "Streaming done", text: "Streamed " + payload.local + " to " + payload.remote + " on SD, took " + payload.time + " seconds"});
|
||||
gcodeFilesViewModel.requestData(payload.remote, "sdcard");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,17 @@ $(function() {
|
|||
//~~ Gcode upload
|
||||
|
||||
function gcode_upload_done(e, data) {
|
||||
gcodeFilesViewModel.fromResponse(data.result);
|
||||
var filename = undefined;
|
||||
var location = undefined;
|
||||
if (data.result.files.hasOwnProperty("sdcard")) {
|
||||
filename = data.result.files.sdcard.name;
|
||||
location = "sdcard";
|
||||
} else if (data.result.files.hasOwnProperty("local")) {
|
||||
filename = data.result.files.local.name;
|
||||
location = "local";
|
||||
}
|
||||
gcodeFilesViewModel.requestData(filename, location);
|
||||
|
||||
if (data.result.done) {
|
||||
$("#gcode_upload_progress .bar").css("width", "0%");
|
||||
$("#gcode_upload_progress").removeClass("progress-striped").removeClass("active");
|
||||
|
|
@ -107,7 +117,7 @@ $(function() {
|
|||
|
||||
function enable_local_dropzone() {
|
||||
$("#gcode_upload").fileupload({
|
||||
url: API_BASEURL + "gcodefiles/local",
|
||||
url: API_BASEURL + "files/local",
|
||||
dataType: "json",
|
||||
dropZone: localTarget,
|
||||
done: gcode_upload_done,
|
||||
|
|
@ -118,7 +128,7 @@ $(function() {
|
|||
|
||||
function disable_local_dropzone() {
|
||||
$("#gcode_upload").fileupload({
|
||||
url: API_BASEURL + "gcodefiles/local",
|
||||
url: API_BASEURL + "files/local",
|
||||
dataType: "json",
|
||||
dropZone: null,
|
||||
done: gcode_upload_done,
|
||||
|
|
@ -129,7 +139,7 @@ $(function() {
|
|||
|
||||
function enable_sd_dropzone() {
|
||||
$("#gcode_upload_sd").fileupload({
|
||||
url: API_BASEURL + "gcodefiles/sdcard",
|
||||
url: API_BASEURL + "files/sdcard",
|
||||
dataType: "json",
|
||||
dropZone: $("#drop_sd"),
|
||||
done: gcode_upload_done,
|
||||
|
|
@ -140,10 +150,9 @@ $(function() {
|
|||
|
||||
function disable_sd_dropzone() {
|
||||
$("#gcode_upload_sd").fileupload({
|
||||
url: API_BASEURL + "gcodefiles/sdcard",
|
||||
url: API_BASEURL + "files/sdcard",
|
||||
dataType: "json",
|
||||
dropZone: null,
|
||||
formData: {target: "sd"},
|
||||
done: gcode_upload_done,
|
||||
fail: gcode_upload_fail,
|
||||
progressall: gcode_upload_progress
|
||||
|
|
@ -282,6 +291,7 @@ $(function() {
|
|||
ko.applyBindings(terminalViewModel, document.getElementById("term"));
|
||||
var gcode = document.getElementById("gcode");
|
||||
if (gcode) {
|
||||
gcodeViewModel.initialize();
|
||||
ko.applyBindings(gcodeViewModel, gcode);
|
||||
}
|
||||
ko.applyBindings(settingsViewModel, document.getElementById("settings_dialog"));
|
||||
|
|
@ -293,10 +303,6 @@ $(function() {
|
|||
if (timelapseElement) {
|
||||
ko.applyBindings(timelapseViewModel, timelapseElement);
|
||||
}
|
||||
var gCodeVisualizerElement = document.getElementById("gcode");
|
||||
if (gCodeVisualizerElement) {
|
||||
gcodeViewModel.initialize();
|
||||
}
|
||||
|
||||
//~~ startup commands
|
||||
|
||||
|
|
|
|||
|
|
@ -96,56 +96,56 @@ function GcodeFilesViewModel(printerStateViewModel, loginStateViewModel) {
|
|||
self.isSdReady(data.flags.sdReady);
|
||||
}
|
||||
|
||||
self.requestData = function(filenameOverride) {
|
||||
self.requestData = function(filenameToFocus, locationToFocus) {
|
||||
$.ajax({
|
||||
url: API_BASEURL + "gcodefiles",
|
||||
url: API_BASEURL + "files",
|
||||
method: "GET",
|
||||
dataType: "json",
|
||||
success: function(response) {
|
||||
if (filenameOverride) {
|
||||
response.filename = filenameOverride
|
||||
}
|
||||
self.fromResponse(response);
|
||||
self.fromResponse(response, filenameToFocus, locationToFocus);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
self.fromResponse = function(response) {
|
||||
self.listHelper.updateItems(response.files);
|
||||
self.fromResponse = function(response, filenameToFocus, locationToFocus) {
|
||||
var files = response.files;
|
||||
_.each(files, function(element, index, list) {
|
||||
if (!element.hasOwnProperty("size")) element.size = "n/a";
|
||||
if (!element.hasOwnProperty("date")) element.date = "n/a";
|
||||
});
|
||||
self.listHelper.updateItems(files);
|
||||
|
||||
if (response.filename) {
|
||||
if (filenameToFocus) {
|
||||
// got a file to scroll to
|
||||
self.listHelper.switchToItem(function(item) {return item.name == response.filename});
|
||||
if (locationToFocus === undefined) {
|
||||
locationToFocus = "local";
|
||||
}
|
||||
self.listHelper.switchToItem(function(item) {return item.name == filenameToFocus && item.origin == locationToFocus});
|
||||
}
|
||||
|
||||
self.freeSpace(response.free);
|
||||
if (response.free) {
|
||||
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;
|
||||
|
||||
var origin;
|
||||
if (file.origin === undefined) {
|
||||
origin = "local";
|
||||
} else {
|
||||
origin = file.origin;
|
||||
}
|
||||
if (!file || !file.refs || !file.refs.hasOwnProperty("resource")) return;
|
||||
|
||||
$.ajax({
|
||||
url: API_BASEURL + "gcodefiles/" + origin + "/" + filename,
|
||||
url: file.refs.resource,
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
contentType: "application/json; charset=UTF-8",
|
||||
data: JSON.stringify({command: "load", print: printAfterLoad})
|
||||
data: JSON.stringify({command: "select", print: printAfterLoad})
|
||||
})
|
||||
}
|
||||
|
||||
self.removeFile = function(filename) {
|
||||
var file = self.listHelper.getItem(function(item) {return item.name == filename});
|
||||
if (!file) return;
|
||||
if (!file || !file.refs || !file.refs.hasOwnProperty("resource")) return;
|
||||
|
||||
var origin;
|
||||
if (file.origin === undefined) {
|
||||
|
|
@ -155,9 +155,8 @@ function GcodeFilesViewModel(printerStateViewModel, loginStateViewModel) {
|
|||
}
|
||||
|
||||
$.ajax({
|
||||
url: API_BASEURL + "gcodefiles/" + origin + "/" + filename,
|
||||
type: "DELETE",
|
||||
success: self.fromResponse
|
||||
url: file.refs.resource,
|
||||
type: "DELETE"
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -175,7 +174,7 @@ function GcodeFilesViewModel(printerStateViewModel, loginStateViewModel) {
|
|||
|
||||
self._sendSdCommand = function(command) {
|
||||
$.ajax({
|
||||
url: API_BASEURL + "control/sd",
|
||||
url: API_BASEURL + "control/printer/sd",
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
contentType: "application/json; charset=UTF-8",
|
||||
|
|
@ -37,6 +37,7 @@ function FirstRunViewModel() {
|
|||
$("#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").unbind("click");
|
||||
$("#confirmation_dialog .confirmation_dialog_acknowledge").click(function(e) {
|
||||
e.preventDefault();
|
||||
$("#confirmation_dialog").modal("hide");
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ function GcodeViewModel(loginStateViewModel) {
|
|||
if (self.status == 'idle' && self.errorCount < 3) {
|
||||
self.status = 'request';
|
||||
$.ajax({
|
||||
url: BASEURL + "downloads/gcode/" + filename,
|
||||
url: BASEURL + "downloads/files/local/" + filename,
|
||||
data: { "mtime": mtime },
|
||||
type: "GET",
|
||||
success: function(response, rstatus) {
|
||||
|
|
@ -67,7 +67,7 @@ function GcodeViewModel(loginStateViewModel) {
|
|||
}
|
||||
}
|
||||
self.errorCount = 0
|
||||
} else if (data.job.filename) {
|
||||
} else if (data.job.filename && !data.job.sd) {
|
||||
self.loadFile(data.job.filename, data.job.mtime);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,16 +120,17 @@ function PrinterStateViewModel(loginStateViewModel) {
|
|||
}
|
||||
|
||||
self.print = function() {
|
||||
var printAction = function() {
|
||||
self._jobCommand("start");
|
||||
var restartCommand = function() {
|
||||
self._jobCommand("restart");
|
||||
}
|
||||
|
||||
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 .confirmation_dialog_acknowledge").unbind("click");
|
||||
$("#confirmation_dialog .confirmation_dialog_acknowledge").click(function(e) {e.preventDefault(); $("#confirmation_dialog").modal("hide"); restartCommand(); });
|
||||
$("#confirmation_dialog").modal("show");
|
||||
} else {
|
||||
printAction();
|
||||
self._jobCommand("start");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ function TemperatureViewModel(loginStateViewModel, settingsViewModel) {
|
|||
data[group][type] = parseInt(temp);
|
||||
|
||||
$.ajax({
|
||||
url: API_BASEURL + "control/printer/hotend",
|
||||
url: API_BASEURL + "control/printer/heater",
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
contentType: "application/json; charset=UTF-8",
|
||||
|
|
|
|||
|
|
@ -76,10 +76,13 @@ function TimelapseViewModel(loginStateViewModel) {
|
|||
};
|
||||
|
||||
self.fromResponse = function(response) {
|
||||
self.timelapseType(response.type);
|
||||
var config = response.config;
|
||||
if (config === undefined) return;
|
||||
|
||||
self.timelapseType(config.type);
|
||||
self.listHelper.updateItems(response.files);
|
||||
|
||||
if (response.type == "timed" && response.config && response.config.interval) {
|
||||
if (config.type == "timed" && response.config.interval) {
|
||||
self.timelapseTimedInterval(response.config.interval);
|
||||
} else {
|
||||
self.timelapseTimedInterval(undefined);
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
<script lang="javascript">
|
||||
var BASEURL = "{{ url_for('index') }}";
|
||||
var API_BASEURL = BASEURL + "ajax/";
|
||||
var API_BASEURL = BASEURL + "api/";
|
||||
|
||||
var CONFIG_GCODEFILESPERPAGE = 5;
|
||||
var CONFIG_TIMELAPSEFILESPERPAGE = 10;
|
||||
|
|
@ -210,18 +210,18 @@
|
|||
<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-bind="enable: loginState.isUser()">
|
||||
<input id="gcode_upload" type="file" name="file" class="fileinput-button" 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-bind="enable: loginState.isUser() && isSdReady()">
|
||||
<input id="gcode_upload_sd" type="file" name="file" class="fileinput-button" 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-bind="enable: loginState.isUser()">
|
||||
<input id="gcode_upload" type="file" name="file" class="fileinput-button" data-bind="enable: loginState.isUser()">
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
@ -656,7 +656,7 @@
|
|||
<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/files.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>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import sys
|
|||
import octoprint.util as util
|
||||
|
||||
from octoprint.settings import settings
|
||||
from octoprint.events import eventManager
|
||||
from octoprint.events import eventManager, Events
|
||||
|
||||
# currently configured timelapse
|
||||
current = None
|
||||
|
|
@ -104,10 +104,10 @@ class Timelapse(object):
|
|||
self._captureMutex = threading.Lock()
|
||||
|
||||
# subscribe events
|
||||
eventManager().subscribe("PrintStarted", self.onPrintStarted)
|
||||
eventManager().subscribe("PrintFailed", self.onPrintDone)
|
||||
eventManager().subscribe("PrintDone", self.onPrintDone)
|
||||
eventManager().subscribe("PrintResumed", self.onPrintResumed)
|
||||
eventManager().subscribe(Events.PRINT_STARTED, self.onPrintStarted)
|
||||
eventManager().subscribe(Events.PRINT_FAILED, self.onPrintDone)
|
||||
eventManager().subscribe(Events.PRINT_DONE, self.onPrintDone)
|
||||
eventManager().subscribe(Events.PRINT_RESUMED, self.onPrintResumed)
|
||||
for (event, callback) in self.eventSubscriptions():
|
||||
eventManager().subscribe(event, callback)
|
||||
|
||||
|
|
@ -116,10 +116,10 @@ class Timelapse(object):
|
|||
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)
|
||||
eventManager().unsubscribe(Events.PRINT_STARTED, self.onPrintStarted)
|
||||
eventManager().unsubscribe(Events.PRINT_FAILED, self.onPrintDone)
|
||||
eventManager().unsubscribe(Events.PRINT_DONE, self.onPrintDone)
|
||||
eventManager().unsubscribe(Events.PRINT_RESUMED, self.onPrintResumed)
|
||||
for (event, callback) in self.eventSubscriptions():
|
||||
eventManager().unsubscribe(event, callback)
|
||||
|
||||
|
|
@ -127,20 +127,20 @@ class Timelapse(object):
|
|||
"""
|
||||
Override this to perform additional actions upon start of a print job.
|
||||
"""
|
||||
self.startTimelapse(payload)
|
||||
self.startTimelapse(payload["file"])
|
||||
|
||||
def onPrintDone(self, event, payload):
|
||||
"""
|
||||
Override this to perform additional actions upon the stop of a print job.
|
||||
"""
|
||||
self.stopTimelapse()
|
||||
self.stopTimelapse(success=(event==Events.PRINT_DONE))
|
||||
|
||||
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)
|
||||
self.startTimelapse(payload["file"])
|
||||
|
||||
def eventSubscriptions(self):
|
||||
"""
|
||||
|
|
@ -172,11 +172,11 @@ class Timelapse(object):
|
|||
self._inTimelapse = True
|
||||
self._gcodeFile = os.path.basename(gcodeFile)
|
||||
|
||||
def stopTimelapse(self, doCreateMovie=True):
|
||||
def stopTimelapse(self, doCreateMovie=True, success=True):
|
||||
self._logger.debug("Stopping timelapse")
|
||||
|
||||
if doCreateMovie:
|
||||
self._renderThread = threading.Thread(target=self._createMovie)
|
||||
self._renderThread = threading.Thread(target=self._createMovie, kwargs={"success": success})
|
||||
self._renderThread.daemon = True
|
||||
self._renderThread.start()
|
||||
|
||||
|
|
@ -197,12 +197,12 @@ class Timelapse(object):
|
|||
captureThread.start()
|
||||
|
||||
def _captureWorker(self, filename):
|
||||
eventManager().fire("CaptureStart", filename);
|
||||
eventManager().fire(Events.CAPTURE_START, {"file": filename});
|
||||
urllib.urlretrieve(self._snapshotUrl, filename)
|
||||
self._logger.debug("Image %s captured from %s" % (filename, self._snapshotUrl))
|
||||
eventManager().fire("CaptureDone", filename);
|
||||
eventManager().fire(Events.CAPTURE_DONE, {"file": filename});
|
||||
|
||||
def _createMovie(self):
|
||||
def _createMovie(self, success=True):
|
||||
ffmpeg = settings().get(["webcam", "ffmpeg"])
|
||||
bitrate = settings().get(["webcam", "bitrate"])
|
||||
if ffmpeg is None or bitrate is None:
|
||||
|
|
@ -210,7 +210,10 @@ class Timelapse(object):
|
|||
return
|
||||
|
||||
input = os.path.join(self._captureDir, "tmp_%05d.jpg")
|
||||
output = os.path.join(self._movieDir, "%s_%s.mpg" % (os.path.splitext(self._gcodeFile)[0], time.strftime("%Y%m%d%H%M%S")))
|
||||
if success:
|
||||
output = os.path.join(self._movieDir, "%s_%s.mpg" % (os.path.splitext(self._gcodeFile)[0], time.strftime("%Y%m%d%H%M%S")))
|
||||
else:
|
||||
output = os.path.join(self._movieDir, "%s_%s-failed.mpg" % (os.path.splitext(self._gcodeFile)[0], time.strftime("%Y%m%d%H%M%S")))
|
||||
|
||||
# prepare ffmpeg command
|
||||
command = [
|
||||
|
|
@ -252,13 +255,13 @@ class Timelapse(object):
|
|||
# finalize command with output file
|
||||
self._logger.debug("Rendering movie to %s" % output)
|
||||
command.append(output)
|
||||
eventManager().fire("MovieRendering", {"gcode": self._gcodeFile, "movie": output, "movie_basename": os.path.basename(output)})
|
||||
eventManager().fire(Events.MOVIE_RENDERING, {"gcode": self._gcodeFile, "movie": output, "movie_basename": os.path.basename(output)})
|
||||
try:
|
||||
subprocess.check_call(command)
|
||||
eventManager().fire("MovieDone", {"gcode": self._gcodeFile, "movie": output, "movie_basename": os.path.basename(output)})
|
||||
eventManager().fire(Events.MOVIE_DONE, {"gcode": self._gcodeFile, "movie": output, "movie_basename": os.path.basename(output)})
|
||||
except subprocess.CalledProcessError as (e):
|
||||
self._logger.warn("Could not render movie, got return code %r" % e.returncode)
|
||||
eventManager().fire("MovieFailed", {"gcode": self._gcodeFile, "movie": output, "movie_basename": os.path.basename(output), "returncode": e.returncode})
|
||||
eventManager().fire(Events.MOVIE_FAILED, {"gcode": self._gcodeFile, "movie": output, "movie_basename": os.path.basename(output), "returncode": e.returncode})
|
||||
|
||||
def cleanCaptureDir(self):
|
||||
if not os.path.isdir(self._captureDir):
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ from octoprint.util.avr_isp import stk500v2
|
|||
from octoprint.util.avr_isp import ispBase
|
||||
|
||||
from octoprint.settings import settings
|
||||
from octoprint.events import eventManager
|
||||
from octoprint.events import eventManager, Events
|
||||
from octoprint.filemanager.destinations import FileDestinations
|
||||
from octoprint.gcodefiles import isGcodeFileName
|
||||
from octoprint.util import getExceptionString, getNewTimeout
|
||||
from octoprint.util.virtual import VirtualPrinter
|
||||
|
|
@ -68,17 +69,32 @@ def baudrateList():
|
|||
return ret
|
||||
|
||||
gcodeToEvent = {
|
||||
"M226": "Waiting", # pause for user input
|
||||
"M0": "Waiting",
|
||||
"M1": "Waiting",
|
||||
"M245": "Cooling", # part cooler
|
||||
"M240": "Conveyor", # part conveyor
|
||||
"M40": "Eject", # part ejector
|
||||
"M300": "Alert", # user alert
|
||||
"G28": "Home", # home print head
|
||||
"M112": "EStop",
|
||||
"M80": "PowerOn",
|
||||
"M81": "PowerOff"
|
||||
# pause for user input
|
||||
"M226": Events.WAITING,
|
||||
"M0": Events.WAITING,
|
||||
"M1": Events.WAITING,
|
||||
|
||||
# part cooler
|
||||
"M245": Events.COOLING,
|
||||
|
||||
# part conveyor
|
||||
"M240": Events.CONVEYOR,
|
||||
|
||||
# part ejector
|
||||
"M40": Events.EJECT,
|
||||
|
||||
# user alert
|
||||
"M300": Events.ALERT,
|
||||
|
||||
# home print head
|
||||
"G28": Events.HOME,
|
||||
|
||||
# emergency stop
|
||||
"M112": Events.E_STOP,
|
||||
|
||||
# motors on/off
|
||||
"M80": Events.POWER_ON,
|
||||
"M81": Events.POWER_OFF,
|
||||
}
|
||||
|
||||
class MachineCom(object):
|
||||
|
|
@ -310,8 +326,14 @@ class MachineCom(object):
|
|||
self._sdFileList = []
|
||||
|
||||
if printing:
|
||||
eventManager().fire("PrintFailed")
|
||||
eventManager().fire("Disconnected")
|
||||
payload = None
|
||||
if self._currentFile is not None:
|
||||
payload = {
|
||||
"file": self._currentFile.getFilename(),
|
||||
"origin": self._currentFile.getFileLocation()
|
||||
}
|
||||
eventManager().fire(Events.PRINT_FAILED, payload)
|
||||
eventManager().fire(Events.DISCONNECTED)
|
||||
|
||||
def setTemperatureOffset(self, extruder=None, bed=None):
|
||||
if extruder is not None:
|
||||
|
|
@ -337,7 +359,10 @@ class MachineCom(object):
|
|||
wasPaused = self.isPaused()
|
||||
self._printSection = "CUSTOM"
|
||||
self._changeState(self.STATE_PRINTING)
|
||||
eventManager().fire("PrintStarted", self._currentFile.getFilename())
|
||||
eventManager().fire(Events.PRINT_STARTED, {
|
||||
"file": self._currentFile.getFilename(),
|
||||
"origin": self._currentFile.getFileLocation()
|
||||
})
|
||||
|
||||
try:
|
||||
self._currentFile.start()
|
||||
|
|
@ -351,18 +376,18 @@ class MachineCom(object):
|
|||
except:
|
||||
self._errorValue = getExceptionString()
|
||||
self._changeState(self.STATE_ERROR)
|
||||
eventManager().fire("Error", self.getErrorString())
|
||||
eventManager().fire(Events.ERROR, self.getErrorString())
|
||||
|
||||
def startFileTransfer(self, filename, remoteFilename):
|
||||
def startFileTransfer(self, filename, localFilename, remoteFilename):
|
||||
if not self.isOperational() or self.isBusy():
|
||||
logging.info("Printer is not operation or busy")
|
||||
return
|
||||
|
||||
self._currentFile = StreamingGcodeFileInformation(filename)
|
||||
self._currentFile = StreamingGcodeFileInformation(filename, localFilename, remoteFilename)
|
||||
self._currentFile.start()
|
||||
|
||||
self.sendCommand("M28 %s" % remoteFilename)
|
||||
eventManager().fire("TransferStart", remoteFilename)
|
||||
eventManager().fire(Events.TRANSFER_STARTED, {"local": localFilename, "remote": remoteFilename})
|
||||
self._callback.mcFileTransferStarted(remoteFilename, self._currentFile.getFilesize())
|
||||
|
||||
def selectFile(self, filename, sd):
|
||||
|
|
@ -376,7 +401,10 @@ class MachineCom(object):
|
|||
self.sendCommand("M23 %s" % filename)
|
||||
else:
|
||||
self._currentFile = PrintingGcodeFileInformation(filename, self.getOffsets)
|
||||
eventManager().fire("FileSelected", filename)
|
||||
eventManager().fire(Events.FILE_SELECTED, {
|
||||
"file": self._currentFile.getFilename(),
|
||||
"origin": self._currentFile.getFileLocation()
|
||||
})
|
||||
self._callback.mcFileSelected(filename, self._currentFile.getFilesize(), False)
|
||||
|
||||
def unselectFile(self):
|
||||
|
|
@ -384,7 +412,7 @@ class MachineCom(object):
|
|||
return
|
||||
|
||||
self._currentFile = None
|
||||
eventManager().fire("FileSelected", None)
|
||||
eventManager().fire(Events.FILE_DESELECTED)
|
||||
self._callback.mcFileSelected(None, None, False)
|
||||
|
||||
def cancelPrint(self):
|
||||
|
|
@ -397,7 +425,10 @@ class MachineCom(object):
|
|||
self.sendCommand("M25") # pause print
|
||||
self.sendCommand("M26 S0") # reset position in file to byte 0
|
||||
|
||||
eventManager().fire("PrintCancelled")
|
||||
eventManager().fire(Events.PRINT_CANCELLED, {
|
||||
"file": self._currentFile.getFilename(),
|
||||
"origin": self._currentFile.getFileLocation()
|
||||
})
|
||||
|
||||
def setPause(self, pause):
|
||||
if self.isStreaming():
|
||||
|
|
@ -409,13 +440,20 @@ class MachineCom(object):
|
|||
self.sendCommand("M24")
|
||||
else:
|
||||
self._sendNext()
|
||||
eventManager().fire("PrintResumed", self._currentFile.getFilename())
|
||||
if pause and self.isPrinting():
|
||||
|
||||
eventManager().fire(Events.PRINT_RESUMED, {
|
||||
"file": self._currentFile.getFilename(),
|
||||
"origin": self._currentFile.getFileLocation()
|
||||
})
|
||||
elif pause and self.isPrinting():
|
||||
self._changeState(self.STATE_PAUSED)
|
||||
if self.isSdFileSelected():
|
||||
self.sendCommand("M25") # pause print
|
||||
|
||||
eventManager().fire("Paused")
|
||||
eventManager().fire(Events.PRINT_PAUSED, {
|
||||
"file": self._currentFile.getFilename(),
|
||||
"origin": self._currentFile.getFileLocation()
|
||||
})
|
||||
|
||||
def getSdFiles(self):
|
||||
return self._sdFiles
|
||||
|
|
@ -560,7 +598,10 @@ class MachineCom(object):
|
|||
# final answer to M23, at least on Marlin, Repetier and Sprinter: "File selected"
|
||||
if self._currentFile is not None:
|
||||
self._callback.mcFileSelected(self._currentFile.getFilename(), self._currentFile.getFilesize(), True)
|
||||
eventManager().fire("FileSelected", self._currentFile.getFilename())
|
||||
eventManager().fire(Events.FILE_SELECTED, {
|
||||
"file": self._currentFile.getFilename(),
|
||||
"origin": self._currentFile.getFileLocation()
|
||||
})
|
||||
elif 'Writing to file' in line:
|
||||
# anwer to M28, at least on Marlin, Repetier and Sprinter: "Writing to file: %s"
|
||||
self._printSection = "CUSTOM"
|
||||
|
|
@ -571,7 +612,10 @@ class MachineCom(object):
|
|||
self._sdFilePos = 0
|
||||
self._callback.mcPrintjobDone()
|
||||
self._changeState(self.STATE_OPERATIONAL)
|
||||
eventManager().fire("PrintDone")
|
||||
eventManager().fire(Events.PRINT_DONE, {
|
||||
"file": self._currentFile.getFilename(),
|
||||
"origin": self._currentFile.getFileLocation()
|
||||
})
|
||||
elif 'Done saving file' in line:
|
||||
self.refreshSdFiles()
|
||||
|
||||
|
|
@ -625,7 +669,7 @@ class MachineCom(object):
|
|||
self.close()
|
||||
self._errorValue = "No more baudrates to test, and no suitable baudrate found."
|
||||
self._changeState(self.STATE_ERROR)
|
||||
eventManager().fire("Error", self.getErrorString())
|
||||
eventManager().fire(Events.ERROR, self.getErrorString())
|
||||
elif self._baudrateDetectRetry > 0:
|
||||
self._baudrateDetectRetry -= 1
|
||||
self._serial.write('\n')
|
||||
|
|
@ -657,7 +701,7 @@ class MachineCom(object):
|
|||
self._changeState(self.STATE_OPERATIONAL)
|
||||
if self._sdAvailable:
|
||||
self.refreshSdFiles()
|
||||
eventManager().fire("Connected", "%s at %s baud" % (self._port, self._baudrate))
|
||||
eventManager().fire(Events.CONNECTED, {"port": self._port, "baudrate": self._baudrate})
|
||||
else:
|
||||
self._testingBaudrate = False
|
||||
|
||||
|
|
@ -671,7 +715,7 @@ class MachineCom(object):
|
|||
self._changeState(self.STATE_OPERATIONAL)
|
||||
if not self._sdAvailable:
|
||||
self.initSdCard()
|
||||
eventManager().fire("Connected", "%s at %s baud" % (self._port, self._baudrate))
|
||||
eventManager().fire(Events.CONNECTED, {"port": self._port, "baudrate": self._baudrate})
|
||||
elif time.time() > timeout:
|
||||
self.close()
|
||||
|
||||
|
|
@ -736,7 +780,7 @@ class MachineCom(object):
|
|||
self._log(errorMsg)
|
||||
self._errorValue = errorMsg
|
||||
self._changeState(self.STATE_ERROR)
|
||||
eventManager().fire("Error", self.getErrorString())
|
||||
eventManager().fire(Events.ERROR, self.getErrorString())
|
||||
self._log("Connection closed, closing down monitor")
|
||||
|
||||
def _openSerial(self):
|
||||
|
|
@ -760,7 +804,7 @@ class MachineCom(object):
|
|||
self._log("Failed to autodetect serial port")
|
||||
self._errorValue = 'Failed to autodetect serial port.'
|
||||
self._changeState(self.STATE_ERROR)
|
||||
eventManager().fire("Error", self.getErrorString())
|
||||
eventManager().fire(Events.ERROR, self.getErrorString())
|
||||
return False
|
||||
elif self._port == 'VIRTUAL':
|
||||
self._changeState(self.STATE_OPEN_SERIAL)
|
||||
|
|
@ -777,7 +821,7 @@ class MachineCom(object):
|
|||
self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString()))
|
||||
self._errorValue = "Failed to open serial port, permissions correct?"
|
||||
self._changeState(self.STATE_ERROR)
|
||||
eventManager().fire("Error", self.getErrorString())
|
||||
eventManager().fire(Events.ERROR, self.getErrorString())
|
||||
return False
|
||||
return True
|
||||
|
||||
|
|
@ -802,7 +846,7 @@ class MachineCom(object):
|
|||
elif not self.isError():
|
||||
self._errorValue = line[6:]
|
||||
self._changeState(self.STATE_ERROR)
|
||||
eventManager().fire("Error", self.getErrorString())
|
||||
eventManager().fire(Events.ERROR, self.getErrorString())
|
||||
return line
|
||||
|
||||
def _readline(self):
|
||||
|
|
@ -827,16 +871,27 @@ class MachineCom(object):
|
|||
if line is None:
|
||||
if self.isStreaming():
|
||||
self._sendCommand("M29")
|
||||
|
||||
filename = self._currentFile.getFilename()
|
||||
payload = {
|
||||
"local": self._currentFile.getLocalFilename(),
|
||||
"remote": self._currentFile.getRemoteFilename(),
|
||||
"time": "%.2f" % (time.time() - self._currentFile.getStartTime())
|
||||
}
|
||||
|
||||
self._currentFile = None
|
||||
self._changeState(self.STATE_OPERATIONAL)
|
||||
self._callback.mcFileTransferDone(filename)
|
||||
eventManager().fire("TransferDone", filename)
|
||||
eventManager().fire(Events.TRANSFER_DONE, payload)
|
||||
self.refreshSdFiles()
|
||||
else:
|
||||
payload = {
|
||||
"file": self._currentFile.getFilename(),
|
||||
"origin": self._currentFile.getFileLocation()
|
||||
}
|
||||
self._callback.mcPrintjobDone()
|
||||
self._changeState(self.STATE_OPERATIONAL)
|
||||
eventManager().fire("PrintDone", self._currentFile.getFilename())
|
||||
eventManager().fire(Events.PRINT_DONE, payload)
|
||||
return
|
||||
|
||||
self._sendCommand(line, True)
|
||||
|
|
@ -845,7 +900,7 @@ class MachineCom(object):
|
|||
def _handleResendRequest(self, line):
|
||||
lineToResend = None
|
||||
try:
|
||||
lineToResend = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
|
||||
lineToResend = int(line.replace("N:", " ").replace("N", " ").replace(":", " ").split()[-1])
|
||||
except:
|
||||
if "rs" in line:
|
||||
lineToResend = int(line.split()[1])
|
||||
|
|
@ -858,7 +913,7 @@ class MachineCom(object):
|
|||
if self.isPrinting():
|
||||
# abort the print, there's nothing we can do to rescue it now
|
||||
self._changeState(self.STATE_ERROR)
|
||||
eventManager().fire("Error", self.getErrorString())
|
||||
eventManager().fire(Events.ERROR, self.getErrorString())
|
||||
else:
|
||||
# reset resend delta, we can't do anything about it
|
||||
self._resendDelta = None
|
||||
|
|
@ -1064,6 +1119,9 @@ class PrintingFileInformation(object):
|
|||
def getFilepos(self):
|
||||
return self._filepos
|
||||
|
||||
def getFileLocation(self):
|
||||
return FileDestinations.LOCAL
|
||||
|
||||
def getProgress(self):
|
||||
"""
|
||||
The current progress of the file, calculated as relation between file position and absolute size. Returns -1
|
||||
|
|
@ -1100,6 +1158,9 @@ class PrintingSdFileInformation(PrintingFileInformation):
|
|||
"""
|
||||
self._filepos = filepos
|
||||
|
||||
def getFileLocation(self):
|
||||
return FileDestinations.SDCARD
|
||||
|
||||
class PrintingGcodeFileInformation(PrintingFileInformation):
|
||||
"""
|
||||
Encapsulates information regarding an ongoing direct print. Takes care of the needed file handle and ensures
|
||||
|
|
@ -1191,5 +1252,17 @@ class PrintingGcodeFileInformation(PrintingFileInformation):
|
|||
return None
|
||||
|
||||
class StreamingGcodeFileInformation(PrintingGcodeFileInformation):
|
||||
def __init__(self, filename):
|
||||
PrintingGcodeFileInformation.__init__(self, filename, None)
|
||||
def __init__(self, path, localFilename, remoteFilename):
|
||||
PrintingGcodeFileInformation.__init__(self, path, None)
|
||||
self._localFilename = localFilename
|
||||
self._remoteFilename = remoteFilename
|
||||
|
||||
def start(self):
|
||||
PrintingGcodeFileInformation.start(self)
|
||||
self._startTime = time.time()
|
||||
|
||||
def getLocalFilename(self):
|
||||
return self._localFilename
|
||||
|
||||
def getRemoteFilename(self):
|
||||
return self._remoteFilename
|
||||
Loading…
Reference in a new issue