diff --git a/docs/images/octoprint-logo.png b/docs/images/octoprint-logo.png new file mode 100644 index 00000000..41f10936 Binary files /dev/null and b/docs/images/octoprint-logo.png differ diff --git a/src/octoprint/events.py b/src/octoprint/events.py index dc6fcef1..064b75f0 100644 --- a/src/octoprint/events.py +++ b/src/octoprint/events.py @@ -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. diff --git a/src/octoprint/gcodefiles.py b/src/octoprint/gcodefiles.py index 3a1dca9d..22adc219 100644 --- a/src/octoprint/gcodefiles.py +++ b/src/octoprint/gcodefiles.py @@ -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) diff --git a/src/octoprint/printer.py b/src/octoprint/printer.py index 94d4e0ba..8f9711df 100644 --- a/src/octoprint/printer.py +++ b/src/octoprint/printer.py @@ -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 diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index a6a34185..401e6f1d 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -3,7 +3,7 @@ __author__ = "Gina Häußge " __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() diff --git a/src/octoprint/server/ajax/gcodefiles.py b/src/octoprint/server/ajax/gcodefiles.py deleted file mode 100644 index a08f515b..00000000 --- a/src/octoprint/server/ajax/gcodefiles.py +++ /dev/null @@ -1,195 +0,0 @@ -# coding=utf-8 -__author__ = "Gina Häußge " -__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/", 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/", 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/", methods=["GET"]) -def readGcodeFile(filename): - return redirectToTornado(request, url_for("index") + "downloads/gcode/" + filename) - - -@ajax.route("/gcodefiles//", 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//", 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//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) - diff --git a/src/octoprint/server/api.py b/src/octoprint/server/api.py deleted file mode 100644 index acbfd760..00000000 --- a/src/octoprint/server/api.py +++ /dev/null @@ -1,55 +0,0 @@ -# coding=utf-8 -__author__ = "Gina Häußge " -__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) - diff --git a/src/octoprint/server/ajax/__init__.py b/src/octoprint/server/api/__init__.py similarity index 85% rename from src/octoprint/server/ajax/__init__.py rename to src/octoprint/server/api/__init__.py index cd921b8f..c72cbd8b 100644 --- a/src/octoprint/server/ajax/__init__.py +++ b/src/octoprint/server/api/__init__.py @@ -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 diff --git a/src/octoprint/server/ajax/control.py b/src/octoprint/server/api/control.py similarity index 77% rename from src/octoprint/server/ajax/control.py rename to src/octoprint/server/api/control.py index e283a3c2..b5171849 100644 --- a/src/octoprint/server/ajax/control.py +++ b/src/octoprint/server/api/control.py @@ -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()) diff --git a/src/octoprint/server/api/files.py b/src/octoprint/server/api/files.py new file mode 100644 index 00000000..8918601d --- /dev/null +++ b/src/octoprint/server/api/files.py @@ -0,0 +1,275 @@ +# coding=utf-8 +from octoprint.events import Events + +__author__ = "Gina Häußge " +__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/", 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/", 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//", 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//", 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//", 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() + diff --git a/src/octoprint/server/ajax/settings.py b/src/octoprint/server/api/settings.py similarity index 98% rename from src/octoprint/server/ajax/settings.py rename to src/octoprint/server/api/settings.py index a9001898..3203e827 100644 --- a/src/octoprint/server/ajax/settings.py +++ b/src/octoprint/server/api/settings.py @@ -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(): diff --git a/src/octoprint/server/ajax/timelapse.py b/src/octoprint/server/api/timelapse.py similarity index 84% rename from src/octoprint/server/ajax/timelapse.py rename to src/octoprint/server/api/timelapse.py index 5b5e1f87..45aab87c 100644 --- a/src/octoprint/server/ajax/timelapse.py +++ b/src/octoprint/server/api/timelapse.py @@ -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/", methods=["GET"]) +@api.route("/timelapse/", methods=["GET"]) def downloadTimelapse(filename): return redirectToTornado(request, url_for("index") + "downloads/timelapse/" + filename) -@ajax.route("/timelapse/", methods=["DELETE"]) +@api.route("/timelapse/", 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: diff --git a/src/octoprint/server/ajax/users.py b/src/octoprint/server/api/users.py similarity index 89% rename from src/octoprint/server/ajax/users.py rename to src/octoprint/server/api/users.py index ed287f44..9228acc9 100644 --- a/src/octoprint/server/ajax/users.py +++ b/src/octoprint/server/api/users.py @@ -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/", methods=["GET"]) +@api.route("/users/", methods=["GET"]) @restricted_access def getUser(username): if userManager is None: @@ -65,7 +65,7 @@ def getUser(username): abort(403) -@ajax.route("/users/", methods=["PUT"]) +@api.route("/users/", methods=["PUT"]) @restricted_access @admin_permission.require(403) def updateUser(username): @@ -91,7 +91,7 @@ def updateUser(username): abort(404) -@ajax.route("/users/", methods=["DELETE"]) +@api.route("/users/", 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//password", methods=["PUT"]) +@api.route("/users//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//apikey", methods=["DELETE"]) +@api.route("/users//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//apikey", methods=["POST"]) +@api.route("/users//apikey", methods=["POST"]) @restricted_access def generateApikeyForUser(username): if userManager is None: diff --git a/src/octoprint/server/util.py b/src/octoprint/server/util.py index 51b1eeca..befa1285 100644 --- a/src/octoprint/server/util.py +++ b/src/octoprint/server/util.py @@ -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 + diff --git a/src/octoprint/static/js/app/dataupdater.js b/src/octoprint/static/js/app/dataupdater.js index bb95739a..30f66eed 100644 --- a/src/octoprint/static/js/app/dataupdater.js +++ b/src/octoprint/static/js/app/dataupdater.js @@ -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; } diff --git a/src/octoprint/static/js/app/main.js b/src/octoprint/static/js/app/main.js index d2800559..9e0fe6e6 100644 --- a/src/octoprint/static/js/app/main.js +++ b/src/octoprint/static/js/app/main.js @@ -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 diff --git a/src/octoprint/static/js/app/viewmodels/gcodefiles.js b/src/octoprint/static/js/app/viewmodels/files.js similarity index 84% rename from src/octoprint/static/js/app/viewmodels/gcodefiles.js rename to src/octoprint/static/js/app/viewmodels/files.js index d159f1aa..2be71d1e 100644 --- a/src/octoprint/static/js/app/viewmodels/gcodefiles.js +++ b/src/octoprint/static/js/app/viewmodels/files.js @@ -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", diff --git a/src/octoprint/static/js/app/viewmodels/firstrun.js b/src/octoprint/static/js/app/viewmodels/firstrun.js index f2913dc3..89ddc15e 100644 --- a/src/octoprint/static/js/app/viewmodels/firstrun.js +++ b/src/octoprint/static/js/app/viewmodels/firstrun.js @@ -37,6 +37,7 @@ function FirstRunViewModel() { $("#confirmation_dialog .confirmation_dialog_message").html("If you disable Access Control and your OctoPrint " + "installation is accessible from the internet, your printer will be accessible by everyone - " + "that also includes the bad guys!"); + $("#confirmation_dialog .confirmation_dialog_acknowledge").unbind("click"); $("#confirmation_dialog .confirmation_dialog_acknowledge").click(function(e) { e.preventDefault(); $("#confirmation_dialog").modal("hide"); diff --git a/src/octoprint/static/js/app/viewmodels/gcode.js b/src/octoprint/static/js/app/viewmodels/gcode.js index a412be27..9c0c28f9 100644 --- a/src/octoprint/static/js/app/viewmodels/gcode.js +++ b/src/octoprint/static/js/app/viewmodels/gcode.js @@ -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); } } diff --git a/src/octoprint/static/js/app/viewmodels/printerstate.js b/src/octoprint/static/js/app/viewmodels/printerstate.js index b9b53b31..a13d0464 100644 --- a/src/octoprint/static/js/app/viewmodels/printerstate.js +++ b/src/octoprint/static/js/app/viewmodels/printerstate.js @@ -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"); } } diff --git a/src/octoprint/static/js/app/viewmodels/temperature.js b/src/octoprint/static/js/app/viewmodels/temperature.js index c974b21e..2bc97899 100644 --- a/src/octoprint/static/js/app/viewmodels/temperature.js +++ b/src/octoprint/static/js/app/viewmodels/temperature.js @@ -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", diff --git a/src/octoprint/static/js/app/viewmodels/timelapse.js b/src/octoprint/static/js/app/viewmodels/timelapse.js index 96128ff3..9d0ac397 100644 --- a/src/octoprint/static/js/app/viewmodels/timelapse.js +++ b/src/octoprint/static/js/app/viewmodels/timelapse.js @@ -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); diff --git a/src/octoprint/templates/index.jinja2 b/src/octoprint/templates/index.jinja2 index 4e1c735a..16856d0c 100644 --- a/src/octoprint/templates/index.jinja2 +++ b/src/octoprint/templates/index.jinja2 @@ -18,7 +18,7 @@ - + diff --git a/src/octoprint/timelapse.py b/src/octoprint/timelapse.py index a71607d7..cd4e9da1 100644 --- a/src/octoprint/timelapse.py +++ b/src/octoprint/timelapse.py @@ -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): diff --git a/src/octoprint/util/comm.py b/src/octoprint/util/comm.py index 85f58465..235d4df3 100644 --- a/src/octoprint/util/comm.py +++ b/src/octoprint/util/comm.py @@ -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) \ No newline at end of file + 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 \ No newline at end of file