From 7a4bb8b94ea36a6ebf11cc25b1a326d751dd9c33 Mon Sep 17 00:00:00 2001 From: Ross Hendrickson Date: Sun, 8 Sep 2013 19:35:24 -0500 Subject: [PATCH] Remove curaEngine from code --- octoprint/1q | 1008 ++++++++++++++++++++++++++++++++++++ octoprint/cura/__init__.py | 2 +- octoprint/gcodefiles.py | 8 +- octoprint/printer.py | 7 +- octoprint/server.py | 24 +- octoprint/settings.py | 6 +- octoprint/static/js/ui.js | 8 +- 7 files changed, 1038 insertions(+), 25 deletions(-) create mode 100644 octoprint/1q diff --git a/octoprint/1q b/octoprint/1q new file mode 100644 index 00000000..41c1eb4e --- /dev/null +++ b/octoprint/1q @@ -0,0 +1,1008 @@ +# coding=utf-8 +__author__ = "Gina Häußge " +__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' + +from werkzeug.utils import secure_filename +import tornadio2 +from flask import Flask, request, render_template, jsonify, send_from_directory, url_for, current_app, session, abort, make_response +from flask.ext.login import LoginManager, login_user, logout_user, login_required, current_user +from flask.ext.principal import Principal, Permission, RoleNeed, Identity, identity_changed, AnonymousIdentity, identity_loaded, UserNeed + +import os +import threading +import logging, logging.config +import subprocess + +from octoprint.printer import Printer, getConnectionOptions +from octoprint.settings import settings, valid_boolean_trues +import octoprint.timelapse +import octoprint.gcodefiles as gcodefiles +import octoprint.util as util +import octoprint.users as users +from octoprint.filemanager.destinations import FileDestinations + + +import octoprint.events as events + +SUCCESS = {} +BASEURL = "/ajax/" +APIBASEURL = "/api/" + +app = Flask("octoprint") +# Only instantiated by the Server().run() method +# In order that threads don't start too early when running as a Daemon +printer = None +timelapse = None + +gcodeManager = None +userManager = None +eventManager = None + +principals = Principal(app) +admin_permission = Permission(RoleNeed("admin")) +user_permission = Permission(RoleNeed("user")) + +#~~ Printer state + +class PrinterStateConnection(tornadio2.SocketConnection): + def __init__(self, printer, gcodeManager, userManager, eventManager, session, endpoint=None): + tornadio2.SocketConnection.__init__(self, session, endpoint) + + self._logger = logging.getLogger(__name__) + + self._temperatureBacklog = [] + self._temperatureBacklogMutex = threading.Lock() + self._logBacklog = [] + self._logBacklogMutex = threading.Lock() + self._messageBacklog = [] + self._messageBacklogMutex = threading.Lock() + + self._printer = printer + self._gcodeManager = gcodeManager + self._userManager = userManager + self._eventManager = eventManager + + def on_open(self, info): + self._logger.info("New connection from client") + # Use of global here is smelly + self._printer.registerCallback(self) + self._gcodeManager.registerCallback(self) + + self._eventManager.fire("ClientOpened") + self._eventManager.subscribe("MovieDone", self._onMovieDone) + + def on_close(self): + self._logger.info("Closed client connection") + # Use of global here is smelly + self._printer.unregisterCallback(self) + self._gcodeManager.unregisterCallback(self) + + self._eventManager.fire("ClientClosed") + self._eventManager.unsubscribe("MovieDone", self._onMovieDone) + + def on_message(self, message): + pass + + def sendCurrentData(self, data): + # add current temperature, log and message backlogs to sent data + with self._temperatureBacklogMutex: + temperatures = self._temperatureBacklog + self._temperatureBacklog = [] + + with self._logBacklogMutex: + logs = self._logBacklog + self._logBacklog = [] + + with self._messageBacklogMutex: + messages = self._messageBacklog + self._messageBacklog = [] + + data.update({ + "temperatures": temperatures, + "logs": logs, + "messages": messages + }) + self.emit("current", data) + + def sendHistoryData(self, data): + self.emit("history", data) + + def sendUpdateTrigger(self, type): + self.emit("updateTrigger", type) + + def sendFeedbackCommandOutput(self, name, output): + self.emit("feedbackCommandOutput", {"name": name, "output": output}) + + def addLog(self, data): + with self._logBacklogMutex: + self._logBacklog.append(data) + + def addMessage(self, data): + with self._messageBacklogMutex: + self._messageBacklog.append(data) + + def addTemperature(self, data): + with self._temperatureBacklogMutex: + self._temperatureBacklog.append(data) + + def _onMovieDone(self, event, payload): + self.sendUpdateTrigger("timelapseFiles") + +# Did attempt to make webserver an encapsulated class but ended up with __call__ failures + +@app.route("/") +def index(): + branch = None + commit = None + try: + branch, commit = util.getGitInfo() + except: + pass + + return render_template( + "index.jinja2", + ajaxBaseUrl=BASEURL, + webcamStream=settings().get(["webcam", "stream"]), + enableTimelapse=(settings().get(["webcam", "snapshot"]) is not None and settings().get(["webcam", "ffmpeg"]) is not None), + enableGCodeVisualizer=settings().get(["feature", "gCodeVisualizer"]), + enableSystemMenu=settings().get(["system"]) is not None and settings().get(["system", "actions"]) is not None and len(settings().get(["system", "actions"])) > 0, + enableAccessControl=userManager is not None, + enableSdSupport=settings().get(["feature", "sdSupport"]), + gitBranch=branch, + gitCommit=commit + ) + +#~~ Printer control + +@app.route(BASEURL + "control/connection/options", methods=["GET"]) +def connectionOptions(): + return jsonify(getConnectionOptions()) + +@app.route(BASEURL + "control/connection", methods=["POST"]) +@login_required +def connect(): + if "command" in request.values.keys() and request.values["command"] == "connect": + port = None + baudrate = None + if "port" in request.values.keys(): + port = request.values["port"] + if "baudrate" in request.values.keys(): + baudrate = request.values["baudrate"] + if "save" in request.values.keys(): + settings().set(["serial", "port"], port) + settings().setInt(["serial", "baudrate"], baudrate) + settings().save() + if "autoconnect" in request.values.keys(): + settings().setBoolean(["serial", "autoconnect"], True) + settings().save() + printer.connect(port=port, baudrate=baudrate) + elif "command" in request.values.keys() and request.values["command"] == "disconnect": + printer.disconnect() + + return jsonify(SUCCESS) + +@app.route(BASEURL + "control/command", methods=["POST"]) +@login_required +def printerCommand(): + if "application/json" in request.headers["Content-Type"]: + data = request.json + + parameters = {} + if "parameters" in data.keys(): parameters = data["parameters"] + + commands = [] + if "command" in data.keys(): commands = [data["command"]] + elif "commands" in data.keys(): commands = data["commands"] + + commandsToSend = [] + for command in commands: + commandToSend = command + if len(parameters) > 0: + commandToSend = command % parameters + commandsToSend.append(commandToSend) + + printer.commands(commandsToSend) + + return jsonify(SUCCESS) + +@app.route(BASEURL + "control/job", methods=["POST"]) +@login_required +def printJobControl(): + if "command" in request.values.keys(): + if request.values["command"] == "start": + printer.startPrint() + elif request.values["command"] == "pause": + printer.togglePausePrint() + elif request.values["command"] == "cancel": + printer.cancelPrint() + return jsonify(SUCCESS) + +@app.route(BASEURL + "control/temperature", methods=["POST"]) +@login_required +def setTargetTemperature(): + if "temp" in request.values.keys(): + # set target temperature + temp = request.values["temp"] + printer.command("M104 S" + temp) + + if "bedTemp" in request.values.keys(): + # set target bed temperature + bedTemp = request.values["bedTemp"] + printer.command("M140 S" + bedTemp) + + return jsonify(SUCCESS) + +@app.route(BASEURL + "control/jog", methods=["POST"]) +@login_required +def jog(): + if not printer.isOperational() or printer.isPrinting(): + # do not jog when a print job is running or we don't have a connection + return jsonify(SUCCESS) + + (movementSpeedX, movementSpeedY, movementSpeedZ, movementSpeedE) = settings().get(["printerParameters", "movementSpeed", ["x", "y", "z", "e"]]) + if "x" in request.values.keys(): + # jog x + x = request.values["x"] + printer.commands(["G91", "G1 X%s F%d" % (x, movementSpeedX), "G90"]) + if "y" in request.values.keys(): + # jog y + y = request.values["y"] + printer.commands(["G91", "G1 Y%s F%d" % (y, movementSpeedY), "G90"]) + if "z" in request.values.keys(): + # jog z + z = request.values["z"] + printer.commands(["G91", "G1 Z%s F%d" % (z, movementSpeedZ), "G90"]) + if "homeXY" in request.values.keys(): + # home x/y + printer.command("G28 X0 Y0") + if "homeZ" in request.values.keys(): + # home z + printer.command("G28 Z0") + if "extrude" in request.values.keys(): + # extrude/retract + length = request.values["extrude"] + printer.commands(["G91", "G1 E%s F%d" % (length, movementSpeedE), "G90"]) + + return jsonify(SUCCESS) + +@app.route(BASEURL + "control/custom", methods=["GET"]) +def getCustomControls(): + customControls = settings().get(["controls"]) + return jsonify(controls=customControls) + +@app.route(BASEURL + "control/sd", methods=["POST"]) +@login_required +def sdCommand(): + if not settings().getBoolean(["feature", "sdSupport"]) or not printer.isOperational() or printer.isPrinting(): + return jsonify(SUCCESS) + + if "command" in request.values.keys(): + command = request.values["command"] + if command == "init": + printer.initSdCard() + elif command == "refresh": + printer.refreshSdFiles() + elif command == "release": + printer.releaseSdCard() + + return jsonify(SUCCESS) + +#~~ GCODE file handling + +@app.route(BASEURL + "gcodefiles", methods=["GET"]) +def readGcodeFiles(): + files = gcodeManager.getAllFileData() + + sdFileList = printer.getSdFiles() + if sdFileList is not None: + for sdFile in sdFileList: + files.append({ + "name": sdFile, + "size": "n/a", + "bytes": 0, + "date": "n/a", + "origin": "sd" + }) + return jsonify(files=files, free=util.getFormattedSize(util.getFreeBytes(settings().getBaseFolder("uploads")))) + +@app.route(BASEURL + "gcodefiles/", methods=["GET"]) +def readGcodeFile(filename): + return send_from_directory(settings().getBaseFolder("uploads"), filename, as_attachment=True) + +@app.route(BASEURL + "gcodefiles/upload", methods=["POST"]) +@login_required +def uploadGcodeFile(): + if "gcode_file" in request.files.keys(): + file = request.files["gcode_file"] + sd = "target" in request.values.keys() and request.values["target"] == "sd"; + + 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"] + + futureFilename = gcodeManager.getFutureFilename(file) + if futureFilename is None: + return make_response("Can not upload file %s, wrong format?" % file.filename, 400) + + if futureFilename == currentFilename and sd == currentSd and printer.isPrinting() or printer.isPaused(): + # trying to overwrite currently selected file, but it is being printed + return make_response("Trying to overwrite file that is currently being printed: %s" % currentFilename, 403) + + destination = FileDestinations.SDCARD if sd else FileDestinations.LOCAL + + filename = gcodeManager.addFile(file, destination) + + if filename is None: + return make_response("Could not upload the file %s" % file.filename, 500) + + absFilename = gcodeManager.getAbsolutePath(filename) + if sd: + printer.addSdFile(filename, absFilename) + + if currentFilename == filename and currentSd == sd: + # reload file as it was updated + if sd: + printer.selectFile(filename, sd, False) + else: + printer.selectFile(absFilename, sd, False) + + global eventManager + eventManager.fire("Upload", filename) + return jsonify(files=gcodeManager.getAllFileData(), filename=filename) + + +@app.route(BASEURL + "gcodefiles/load", methods=["POST"]) +@login_required +def loadGcodeFile(): + if "filename" in request.values.keys(): + printAfterLoading = False + if "print" in request.values.keys() and request.values["print"] in valid_boolean_trues: + printAfterLoading = True + + sd = False + if "target" in request.values.keys() and request.values["target"] == "sd": + filename = request.values["filename"] + sd = True + else: + filename = gcodeManager.getAbsolutePath(request.values["filename"]) + printer.selectFile(filename, sd, printAfterLoading) + return jsonify(SUCCESS) + +@app.route(BASEURL + "gcodefiles/delete", methods=["POST"]) +@login_required +def deleteGcodeFile(): + if "filename" in request.values.keys(): + filename = request.values["filename"] + sd = "target" in request.values.keys() and request.values["target"] == "sd" + + 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 currentSd: + printer.deleteSdFile(filename) + else: + gcodeManager.removeFile(filename) + return readGcodeFiles() + +@app.route(BASEURL + "gcodefiles/refresh", methods=["POST"]) +@login_required +def refreshFiles(): + printer.updateSdFiles() + return jsonify(SUCCESS) + +#-- very simple api routines +@app.route(APIBASEURL + "load", methods=["POST"]) +def apiLoad(): + logger = logging.getLogger(__name__) + + if not settings().get(["api", "enabled"]): + abort(401) + + if not "apikey" in request.values.keys(): + abort(401) + + if request.values["apikey"] != settings().get(["api", "key"]): + abort(403) + + if not "file" in request.files.keys(): + abort(400) + + # Perform an upload + file = request.files["file"] + filename = gcodeManager.addFile(file) + 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) + +@app.route(APIBASEURL + "state", methods=["GET"]) +def apiPrinterState(): + if not settings().get(["api", "enabled"]): + abort(401) + + if not "apikey" in request.values.keys(): + abort(401) + + if request.values["apikey"] != settings().get(["api", "key"]): + abort(403) + + currentData = printer.getCurrentData() + currentData.update({ + "temperatures": printer.getCurrentTemperatures() + }) + return jsonify(currentData) + +#~~ timelapse handling + +@app.route(BASEURL + "timelapse", methods=["GET"]) +def getTimelapseData(): + global timelapse + + type = "off" + additionalConfig = {} + if timelapse is not None and isinstance(timelapse, octoprint.timelapse.ZTimelapse): + type = "zchange" + elif timelapse is not None and isinstance(timelapse, octoprint.timelapse.TimedTimelapse): + type = "timed" + additionalConfig = { + "interval": timelapse.interval() + } + + files = octoprint.timelapse.getFinishedTimelapses() + for file in files: + file["url"] = url_for("downloadTimelapse", filename=file["name"]) + + return jsonify({ + "type": type, + "config": additionalConfig, + "files": files + }) + +@app.route(BASEURL + "timelapse/", methods=["GET"]) +def downloadTimelapse(filename): + if util.isAllowedFile(filename, set(["mpg"])): + return send_from_directory(settings().getBaseFolder("timelapse"), filename, as_attachment=True) + +@app.route(BASEURL + "timelapse/", methods=["DELETE"]) +@login_required +def deleteTimelapse(filename): + if util.isAllowedFile(filename, set(["mpg"])): + secure = os.path.join(settings().getBaseFolder("timelapse"), secure_filename(filename)) + if os.path.exists(secure): + os.remove(secure) + return getTimelapseData() + +@app.route(BASEURL + "timelapse", methods=["POST"]) +@login_required +def setTimelapseConfig(): + global timelapse + + if request.values.has_key("type"): + type = request.values["type"] + if type in ["zchange", "timed"]: + # valid timelapse type, check if there is an old one we need to stop first + if timelapse is not None: + timelapse.unload() + timelapse = None + if "zchange" == type: + timelapse = octoprint.timelapse.ZTimelapse() + elif "timed" == type: + interval = 10 + if request.values.has_key("interval"): + try: + interval = int(request.values["interval"]) + except ValueError: + pass + timelapse = octoprint.timelapse.TimedTimelapse(interval) + + return getTimelapseData() + +#~~ settings + +@app.route(BASEURL + "settings", methods=["GET"]) +def getSettings(): + s = settings() + + [movementSpeedX, movementSpeedY, movementSpeedZ, movementSpeedE] = s.get(["printerParameters", "movementSpeed", ["x", "y", "z", "e"]]) + + connectionOptions = getConnectionOptions() + + return jsonify({ + "api": { + "enabled": s.getBoolean(["api", "enabled"]), + "key": s.get(["api", "key"]) + }, + "appearance": { + "name": s.get(["appearance", "name"]), + "color": s.get(["appearance", "color"]) + }, + "printer": { + "movementSpeedX": movementSpeedX, + "movementSpeedY": movementSpeedY, + "movementSpeedZ": movementSpeedZ, + "movementSpeedE": movementSpeedE, + }, + "webcam": { + "streamUrl": s.get(["webcam", "stream"]), + "snapshotUrl": s.get(["webcam", "snapshot"]), + "ffmpegPath": s.get(["webcam", "ffmpeg"]), + "bitrate": s.get(["webcam", "bitrate"]), + "watermark": s.getBoolean(["webcam", "watermark"]), + "flipH": s.getBoolean(["webcam", "flipH"]), + "flipV": s.getBoolean(["webcam", "flipV"]) + }, + "feature": { + "gcodeViewer": s.getBoolean(["feature", "gCodeVisualizer"]), + "waitForStart": s.getBoolean(["feature", "waitForStartOnConnect"]), + "alwaysSendChecksum": s.getBoolean(["feature", "alwaysSendChecksum"]), + "sdSupport": s.getBoolean(["feature", "sdSupport"]) + }, + "serial": { + "port": connectionOptions["portPreference"], + "baudrate": connectionOptions["baudratePreference"], + "portOptions": connectionOptions["ports"], + "baudrateOptions": connectionOptions["baudrates"], + "autoconnect": s.getBoolean(["serial", "autoconnect"]), + "timeoutConnection": s.getFloat(["serial", "timeout", "connection"]), + "timeoutDetection": s.getFloat(["serial", "timeout", "detection"]), + "timeoutCommunication": s.getFloat(["serial", "timeout", "communication"]), + "log": s.getBoolean(["serial", "log"]) + }, + "folder": { + "uploads": s.getBaseFolder("uploads"), + "timelapse": s.getBaseFolder("timelapse"), + "timelapseTmp": s.getBaseFolder("timelapse_tmp"), + "logs": s.getBaseFolder("logs") + }, + "temperature": { + "profiles": s.get(["temperature", "profiles"]) + }, + "system": { + "actions": s.get(["system", "actions"]), + "events": s.get(["system", "events"]) + }, + "cura": { + "enabled": s.getBoolean(["cura", "enabled"]), + "path": s.get(["cura", "path"]), + "config": s.get(["cura", "config"]) + } + }) + +@app.route(BASEURL + "settings", methods=["POST"]) +@login_required +@admin_permission.require(403) +def setSettings(): + if "application/json" in request.headers["Content-Type"]: + data = request.json + s = settings() + + if "api" in data.keys(): + if "enabled" in data["api"].keys(): s.set(["api", "enabled"], data["api"]["enabled"]) + if "key" in data["api"].keys(): s.set(["api", "key"], data["api"]["key"], True) + + if "appearance" in data.keys(): + if "name" in data["appearance"].keys(): s.set(["appearance", "name"], data["appearance"]["name"]) + if "color" in data["appearance"].keys(): s.set(["appearance", "color"], data["appearance"]["color"]) + + if "printer" in data.keys(): + if "movementSpeedX" in data["printer"].keys(): s.setInt(["printerParameters", "movementSpeed", "x"], data["printer"]["movementSpeedX"]) + if "movementSpeedY" in data["printer"].keys(): s.setInt(["printerParameters", "movementSpeed", "y"], data["printer"]["movementSpeedY"]) + if "movementSpeedZ" in data["printer"].keys(): s.setInt(["printerParameters", "movementSpeed", "z"], data["printer"]["movementSpeedZ"]) + if "movementSpeedE" in data["printer"].keys(): s.setInt(["printerParameters", "movementSpeed", "e"], data["printer"]["movementSpeedE"]) + + if "webcam" in data.keys(): + if "streamUrl" in data["webcam"].keys(): s.set(["webcam", "stream"], data["webcam"]["streamUrl"]) + if "snapshotUrl" in data["webcam"].keys(): s.set(["webcam", "snapshot"], data["webcam"]["snapshotUrl"]) + if "ffmpegPath" in data["webcam"].keys(): s.set(["webcam", "ffmpeg"], data["webcam"]["ffmpegPath"]) + if "bitrate" in data["webcam"].keys(): s.set(["webcam", "bitrate"], data["webcam"]["bitrate"]) + if "watermark" in data["webcam"].keys(): s.setBoolean(["webcam", "watermark"], data["webcam"]["watermark"]) + if "flipH" in data["webcam"].keys(): s.setBoolean(["webcam", "flipH"], data["webcam"]["flipH"]) + if "flipV" in data["webcam"].keys(): s.setBoolean(["webcam", "flipV"], data["webcam"]["flipV"]) + + if "feature" in data.keys(): + if "gcodeViewer" in data["feature"].keys(): s.setBoolean(["feature", "gCodeVisualizer"], data["feature"]["gcodeViewer"]) + if "waitForStart" in data["feature"].keys(): s.setBoolean(["feature", "waitForStartOnConnect"], data["feature"]["waitForStart"]) + if "alwaysSendChecksum" in data["feature"].keys(): s.setBoolean(["feature", "alwaysSendChecksum"], data["feature"]["alwaysSendChecksum"]) + if "sdSupport" in data["feature"].keys(): s.setBoolean(["feature", "sdSupport"], data["feature"]["sdSupport"]) + + if "serial" in data.keys(): + if "autoconnect" in data["serial"].keys(): s.setBoolean(["serial", "autoconnect"], data["serial"]["autoconnect"]) + if "port" in data["serial"].keys(): s.set(["serial", "port"], data["serial"]["port"]) + if "baudrate" in data["serial"].keys(): s.setInt(["serial", "baudrate"], data["serial"]["baudrate"]) + if "timeoutConnection" in data["serial"].keys(): s.setFloat(["serial", "timeout", "connection"], data["serial"]["timeoutConnection"]) + if "timeoutDetection" in data["serial"].keys(): s.setFloat(["serial", "timeout", "detection"], data["serial"]["timeoutDetection"]) + if "timeoutCommunication" in data["serial"].keys(): s.setFloat(["serial", "timeout", "communication"], data["serial"]["timeoutCommunication"]) + + oldLog = s.getBoolean(["serial", "log"]) + if "log" in data["serial"].keys(): s.setBoolean(["serial", "log"], data["serial"]["log"]) + if oldLog and not s.getBoolean(["serial", "log"]): + # disable debug logging to serial.log + logging.getLogger("SERIAL").debug("Disabling serial logging") + logging.getLogger("SERIAL").setLevel(logging.CRITICAL) + elif not oldLog and s.getBoolean(["serial", "log"]): + # enable debug logging to serial.log + logging.getLogger("SERIAL").setLevel(logging.DEBUG) + logging.getLogger("SERIAL").debug("Enabling serial logging") + + if "folder" in data.keys(): + if "uploads" in data["folder"].keys(): s.setBaseFolder("uploads", data["folder"]["uploads"]) + if "timelapse" in data["folder"].keys(): s.setBaseFolder("timelapse", data["folder"]["timelapse"]) + if "timelapseTmp" in data["folder"].keys(): s.setBaseFolder("timelapse_tmp", data["folder"]["timelapseTmp"]) + if "logs" in data["folder"].keys(): s.setBaseFolder("logs", data["folder"]["logs"]) + + if "temperature" in data.keys(): + if "profiles" in data["temperature"].keys(): s.set(["temperature", "profiles"], data["temperature"]["profiles"]) + + if "system" in data.keys(): + if "actions" in data["system"].keys(): s.set(["system", "actions"], data["system"]["actions"]) + + if "events" in data["system"].keys(): s.set(["system", "events"], data["system"]["events"]) + + cura = data.get("cura", None) + + if cura: + path = cura.get("path") + if path: + s.set(["cura", "path"], path) + + config = cura.get("config") + if config: + s.set(["cura", "config"], config) + + # Enabled is a boolean so we cannot check that we have a result + enabled = cura.get("enabled") + s.setBoolean(["cura", "enabled"], enabled) + + s.save() + + return getSettings() + +#~~ user settings + +@app.route(BASEURL + "users", methods=["GET"]) +@login_required +@admin_permission.require(403) +def getUsers(): + if userManager is None: + return jsonify(SUCCESS) + + return jsonify({"users": userManager.getAllUsers()}) + +@app.route(BASEURL + "users", methods=["POST"]) +@login_required +@admin_permission.require(403) +def addUser(): + if userManager is None: + return jsonify(SUCCESS) + + if "application/json" in request.headers["Content-Type"]: + data = request.json + + name = data["name"] + password = data["password"] + active = data["active"] + + roles = ["user"] + if "admin" in data.keys() and data["admin"]: + roles.append("admin") + + try: + userManager.addUser(name, password, active, roles) + except users.UserAlreadyExists: + abort(409) + return getUsers() + +@app.route(BASEURL + "users/", methods=["GET"]) +@login_required +def getUser(username): + if userManager is None: + return jsonify(SUCCESS) + + if current_user is not None and not current_user.is_anonymous() and (current_user.get_name() == username or current_user.is_admin()): + user = userManager.findUser(username) + if user is not None: + return jsonify(user.asDict()) + else: + abort(404) + else: + abort(403) + +@app.route(BASEURL + "users/", methods=["PUT"]) +@login_required +@admin_permission.require(403) +def updateUser(username): + if userManager is None: + return jsonify(SUCCESS) + + user = userManager.findUser(username) + if user is not None: + if "application/json" in request.headers["Content-Type"]: + data = request.json + + # change roles + roles = ["user"] + if "admin" in data.keys() and data["admin"]: + roles.append("admin") + userManager.changeUserRoles(username, roles) + + # change activation + if "active" in data.keys(): + userManager.changeUserActivation(username, data["active"]) + return getUsers() + else: + abort(404) + +@app.route(BASEURL + "users/", methods=["DELETE"]) +@login_required +@admin_permission.require(http_exception=403) +def removeUser(username): + if userManager is None: + return jsonify(SUCCESS) + + try: + userManager.removeUser(username) + return getUsers() + except users.UnknownUser: + abort(404) + +@app.route(BASEURL + "users//password", methods=["PUT"]) +@login_required +def changePasswordForUser(username): + if userManager is None: + return jsonify(SUCCESS) + + if current_user is not None and not current_user.is_anonymous() and (current_user.get_name() == username or current_user.is_admin()): + if "application/json" in request.headers["Content-Type"]: + data = request.json + if "password" in data.keys() and data["password"]: + try: + userManager.changeUserPassword(username, data["password"]) + except users.UnknownUser: + return app.make_response(("Unknown user: %s" % username, 404, [])) + return jsonify(SUCCESS) + else: + return app.make_response(("Forbidden", 403, [])) + +#~~ system control + +@app.route(BASEURL + "system", methods=["POST"]) +@login_required +@admin_permission.require(403) +def performSystemAction(): + logger = logging.getLogger(__name__) + if request.values.has_key("action"): + action = request.values["action"] + availableActions = settings().get(["system", "actions"]) + for availableAction in availableActions: + if availableAction["action"] == action: + logger.info("Performing command: %s" % availableAction["command"]) + try: + subprocess.check_output(availableAction["command"], shell=True) + except subprocess.CalledProcessError, e: + logger.warn("Command failed with return code %i: %s" % (e.returncode, e.message)) + return app.make_response(("Command failed with return code %i: %s" % (e.returncode, e.message), 500, [])) + except Exception, ex: + logger.exception("Command failed") + return app.make_response(("Command failed: %r" % ex, 500, [])) + return jsonify(SUCCESS) + +#~~ Login/user handling + +@app.route(BASEURL + "login", methods=["POST"]) +def login(): + if userManager is not None and "user" in request.values.keys() and "pass" in request.values.keys(): + username = request.values["user"] + password = request.values["pass"] + + if "remember" in request.values.keys() and request.values["remember"] == "true": + remember = True + else: + remember = False + + user = userManager.findUser(username) + if user is not None: + if user.check_password(users.UserManager.createPasswordHash(password)): + login_user(user, remember=remember) + identity_changed.send(current_app._get_current_object(), identity=Identity(user.get_id())) + return jsonify(user.asDict()) + return app.make_response(("User unknown or password incorrect", 401, [])) + elif "passive" in request.values.keys(): + user = current_user + if user is not None and not user.is_anonymous(): + identity_changed.send(current_app._get_current_object(), identity=Identity(user.get_id())) + return jsonify(user.asDict()) + return jsonify(SUCCESS) + +@app.route(BASEURL + "logout", methods=["POST"]) +@login_required +def logout(): + # Remove session keys set by Flask-Principal + for key in ('identity.id', 'identity.auth_type'): + del session[key] + identity_changed.send(current_app._get_current_object(), identity=AnonymousIdentity()) + + logout_user() + + return jsonify(SUCCESS) + +@identity_loaded.connect_via(app) +def on_identity_loaded(sender, identity): + user = load_user(identity.id) + if user is None: + return + + identity.provides.add(UserNeed(user.get_name())) + if user.is_user(): + identity.provides.add(RoleNeed("user")) + if user.is_admin(): + identity.provides.add(RoleNeed("admin")) + +def load_user(id): + if userManager is not None: + return userManager.findUser(id) + return users.DummyUser() + +#~~ startup code +class Server(): + def __init__(self, configfile=None, basedir=None, host="0.0.0.0", port=5000, debug=False): + self._configfile = configfile + self._basedir = basedir + self._host = host + self._port = port + self._debug = debug + + + def run(self): + # Global as I can't work out a way to get it into PrinterStateConnection + global printer + global gcodeManager + global userManager + global eventManager + + from tornado.wsgi import WSGIContainer + from tornado.httpserver import HTTPServer + from tornado.ioloop import IOLoop + from tornado.web import Application, FallbackHandler + + # first initialize the settings singleton and make sure it uses given configfile and basedir if available + self._initSettings(self._configfile, self._basedir) + + # then initialize logging + self._initLogging(self._debug) + logger = logging.getLogger(__name__) + + eventManager = events.eventManager() + gcodeManager = gcodefiles.GcodeManager() + printer = Printer(gcodeManager) + + # setup system and gcode command triggers + events.SystemCommandTrigger(printer) + events.GcodeCommandTrigger(printer) + if self._debug: + events.DebugEventListener() + + if settings().getBoolean(["accessControl", "enabled"]): + userManagerName = settings().get(["accessControl", "userManager"]) + try: + clazz = util.getClass(userManagerName) + userManager = clazz() + except AttributeError, e: + logger.exception("Could not instantiate user manager %s, will run with accessControl disabled!" % userManagerName) + + app.secret_key = "k3PuVYgtxNm8DXKKTw2nWmFQQun9qceV" + login_manager = LoginManager() + login_manager.session_protection = "strong" + login_manager.user_callback = load_user + if userManager is None: + login_manager.anonymous_user = users.DummyUser + principals.identity_loaders.appendleft(users.dummy_identity_loader) + login_manager.init_app(app) + + if self._host is None: + self._host = settings().get(["server", "host"]) + if self._port is None: + self._port = settings().getInt(["server", "port"]) + + logger.info("Listening on http://%s:%d" % (self._host, self._port)) + app.debug = self._debug + + self._router = tornadio2.TornadioRouter(self._createSocketConnection) + + self._tornado_app = Application(self._router.urls + [ + (".*", FallbackHandler, {"fallback": WSGIContainer(app)}) + ]) + self._server = HTTPServer(self._tornado_app) + self._server.listen(self._port, address=self._host) + + eventManager.fire("Startup") + if settings().getBoolean(["serial", "autoconnect"]): + (port, baudrate) = settings().get(["serial", "port"]), settings().getInt(["serial", "baudrate"]) + connectionOptions = getConnectionOptions() + if port in connectionOptions["ports"]: + printer.connect(port, baudrate) + IOLoop.instance().start() + + def _createSocketConnection(self, session, endpoint=None): + global printer, gcodeManager, userManager, eventManager + return PrinterStateConnection(printer, gcodeManager, userManager, eventManager, session, endpoint) + + def _initSettings(self, configfile, basedir): + s = settings(init=True, basedir=basedir, configfile=configfile) + + def _initLogging(self, debug): + config = { + "version": 1, + "formatters": { + "simple": { + "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + } + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "level": "DEBUG", + "formatter": "simple", + "stream": "ext://sys.stdout" + }, + "file": { + "class": "logging.handlers.TimedRotatingFileHandler", + "level": "DEBUG", + "formatter": "simple", + "when": "D", + "backupCount": "1", + "filename": os.path.join(settings().getBaseFolder("logs"), "octoprint.log") + }, + "serialFile": { + "class": "logging.handlers.RotatingFileHandler", + "level": "DEBUG", + "formatter": "simple", + "maxBytes": 2 * 1024 * 1024, # let's limit the serial log to 2MB in size + "filename": os.path.join(settings().getBaseFolder("logs"), "serial.log") + } + }, + "loggers": { + #"octoprint.timelapse": { + # "level": "DEBUG" + #}, + #"octoprint.events": { + # "level": "DEBUG" + #}, + "SERIAL": { + "level": "CRITICAL", + "handlers": ["serialFile"], + "propagate": False + } + }, + "root": { + "level": "INFO", + "handlers": ["console", "file"] + } + } + + if debug: + config["root"]["level"] = "DEBUG" + + logging.config.dictConfig(config) + +if __name__ == "__main__": + octoprint = Server() + octoprint.run() diff --git a/octoprint/cura/__init__.py b/octoprint/cura/__init__.py index f1cc1983..2d479b93 100644 --- a/octoprint/cura/__init__.py +++ b/octoprint/cura/__init__.py @@ -16,7 +16,7 @@ class CuraFactory(object): if path: return CuraEngine(path) current_settings = settings(init=True) - path = current_settings.get(["curaEngine", "path"]) + path = current_settings.get(["cura", "path"]) return CuraEngine(path) diff --git a/octoprint/gcodefiles.py b/octoprint/gcodefiles.py index ca13614f..e0fc7ce9 100644 --- a/octoprint/gcodefiles.py +++ b/octoprint/gcodefiles.py @@ -138,7 +138,7 @@ class GcodeManager: if isGcodeFileName(filename): return self.processGcode(absolutePath) - curaEnabled = self._settings.get(["curaEngine", "enabled"]) + curaEnabled = self._settings.get(["cura", "enabled"]) if isSTLFileName(filename) and curaEnabled and local: gcodePath = util.genGcodeFileName(absolutePath) @@ -163,11 +163,11 @@ class GcodeManager: from octoprint.cura import CuraFactory - curaEngine = CuraFactory.create_slicer() + cura = CuraFactory.create_slicer() gcodePath = util.genGcodeFileName(absolutePath) - config = self._settings.get(["curaEngine", "config"]) + config = self._settings.get(["cura", "config"]) - curaEngine.process_file( + cura.process_file( config, gcodePath, absolutePath, callBack, callBackArgs) def processGcode(self, absolutePath): if absolutePath is None: diff --git a/octoprint/printer.py b/octoprint/printer.py index 9a240f18..00fb7452 100644 --- a/octoprint/printer.py +++ b/octoprint/printer.py @@ -176,7 +176,12 @@ class Printer(): self._comm.sendCommand(command) def selectFile(self, filename, sd, printAfterSelect=False): - if self._comm is not None and (self._comm.isBusy() or self._comm.isStreaming()): + + if not self._comm: + logging.info("No printer is connected, cannot load file") + return + + if self._comm.isBusy() or self._comm.isStreaming(): return self._printAfterSelect = printAfterSelect diff --git a/octoprint/server.py b/octoprint/server.py index 82aee26c..41c1eb4e 100644 --- a/octoprint/server.py +++ b/octoprint/server.py @@ -580,10 +580,10 @@ def getSettings(): "actions": s.get(["system", "actions"]), "events": s.get(["system", "events"]) }, - "curaEngine": { - "enabled": s.getBoolean(["curaEngine", "enabled"]), - "path": s.get(["curaEngine", "path"]), - "config": s.get(["curaEngine", "config"]) + "cura": { + "enabled": s.getBoolean(["cura", "enabled"]), + "path": s.get(["cura", "path"]), + "config": s.get(["cura", "config"]) } }) @@ -657,20 +657,20 @@ def setSettings(): if "events" in data["system"].keys(): s.set(["system", "events"], data["system"]["events"]) - curaEngine = data.get("curaEngine", None) + cura = data.get("cura", None) - if curaEngine: - path = curaEngine.get("path") + if cura: + path = cura.get("path") if path: - s.set(["curaEngine", "path"], path) + s.set(["cura", "path"], path) - config = curaEngine.get("config") + config = cura.get("config") if config: - s.set(["curaEngine", "config"], config) + s.set(["cura", "config"], config) # Enabled is a boolean so we cannot check that we have a result - enabled = curaEngine.get("enabled") - s.setBoolean(["curaEngine", "enabled"], enabled) + enabled = cura.get("enabled") + s.setBoolean(["cura", "enabled"], enabled) s.save() diff --git a/octoprint/settings.py b/octoprint/settings.py index 00b1b872..c55d2c6f 100644 --- a/octoprint/settings.py +++ b/octoprint/settings.py @@ -89,10 +89,10 @@ default_settings = { "userManager": "octoprint.users.FilebasedUserManager", "userfile": None }, - "curaEngine": { + "cura": { "enabled": False, - "path": "/default/path/to/curaEngine/dir/", - "config": "/default/path/to/your/curaEngine/config/file/" + "path": "/default/path/to/cura", + "config": "/default/path/to/your/cura/config.ini" }, "events": { "systemCommandTrigger": { diff --git a/octoprint/static/js/ui.js b/octoprint/static/js/ui.js index e0b881b8..02b8ffe7 100644 --- a/octoprint/static/js/ui.js +++ b/octoprint/static/js/ui.js @@ -1472,9 +1472,9 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) { self.folder_timelapseTmp(response.folder.timelapseTmp); self.folder_logs(response.folder.logs); - self.cura_enabled(response.curaEngine.enabled); - self.cura_path(response.curaEngine.path); - self.cura_config(response.curaEngine.config); + self.cura_enabled(response.cura.enabled); + self.cura_path(response.cura.path); + self.cura_config(response.cura.config); self.temperature_profiles(response.temperature.profiles); @@ -1533,7 +1533,7 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) { "system": { "actions": self.system_actions() }, - "curaEngine": { + "cura": { "enabled": self.cura_enabled(), "path": self.cura_path(), "config": self.cura_config()