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:
Gina Häußge 2013-12-21 14:46:20 +01:00
parent 049ed723a7
commit eccc9d6fbd
25 changed files with 720 additions and 471 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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