OctoPrint/printer_webui/server.py

355 lines
10 KiB
Python

# 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 Flask, request, render_template, jsonify, send_from_directory, abort, url_for
from werkzeug import secure_filename
from printer_webui.printer import Printer, getConnectionOptions
from printer_webui.settings import settings
import timelapse
import os
import fnmatch
BASEURL = "/ajax/"
SUCCESS = {}
UPLOAD_FOLDER = settings().getBaseFolder("uploads")
app = Flask("printer_webui")
printer = Printer()
@app.route("/")
def index():
return render_template(
"index.html",
webcamStream=settings().get("webcam", "stream"),
enableTimelapse=(settings().get("webcam", "snapshot") is not None and settings().get("webcam", "ffmpeg") is not None)
)
#~~ Printer state
@app.route(BASEURL + "state", methods=["GET"])
def printerState():
temp = printer.currentTemp
bedTemp = printer.currentBedTemp
targetTemp = printer.currentTargetTemp
bedTargetTemp = printer.currentBedTargetTemp
jobData = printer.jobData()
gcodeState = printer.gcodeState()
feedrateState = printer.feedrateState()
result = {
"state": printer.getStateString(),
"temp": temp,
"bedTemp": bedTemp,
"targetTemp": targetTemp,
"targetBedTemp": bedTargetTemp,
"operational": printer.isOperational(),
"closedOrError": printer.isClosedOrError(),
"error": printer.isError(),
"printing": printer.isPrinting(),
"paused": printer.isPaused(),
"ready": printer.isReady(),
"loading": printer.isLoading()
}
if jobData is not None:
jobData["filename"] = jobData["filename"].replace(UPLOAD_FOLDER + os.sep, "")
result["job"] = jobData
if gcodeState is not None:
gcodeState["filename"] = gcodeState["filename"].replace(UPLOAD_FOLDER + os.sep, "")
result["gcode"] = gcodeState
if feedrateState is not None:
result["feedrate"] = feedrateState
if request.values.has_key("temperatures"):
result["temperatures"] = printer.temps
if request.values.has_key("log"):
result["log"] = printer.log
if request.values.has_key("messages"):
result["messages"] = printer.messages
return jsonify(result)
@app.route(BASEURL + "state/messages", methods=["GET"])
def printerMessages():
return jsonify(messages=printer.messages)
@app.route(BASEURL + "state/log", methods=["GET"])
def printerLogs():
return jsonify(log=printer.log)
@app.route(BASEURL + "state/temperatures", methods=["GET"])
def printerTemperatures():
return jsonify(temperatures = printer.temps)
#~~ Printer control
@app.route(BASEURL + "control/connectionOptions", methods=["GET"])
def connectionOptions():
return jsonify(getConnectionOptions())
@app.route(BASEURL + "control/connect", methods=["POST"])
def connect():
port = None
baudrate = None
if request.values.has_key("port"):
port = request.values["port"]
if request.values.has_key("baudrate"):
baudrate = request.values["baudrate"]
if request.values.has_key("save"):
settings().set("serial", "port", port)
settings().set("serial", "baudrate", baudrate)
settings().save()
printer.connect(port=port, baudrate=baudrate)
return jsonify(state="Connecting")
@app.route(BASEURL + "control/disconnect", methods=["POST"])
def disconnect():
printer.disconnect()
return jsonify(state="Offline")
@app.route(BASEURL + "control/command", methods=["POST"])
def printerCommand():
command = request.form["command"]
printer.command(command)
return jsonify(SUCCESS)
@app.route(BASEURL + "control/print", methods=["POST"])
def printGcode():
printer.startPrint()
return jsonify(SUCCESS)
@app.route(BASEURL + "control/pause", methods=["POST"])
def pausePrint():
printer.togglePausePrint()
return jsonify(SUCCESS)
@app.route(BASEURL + "control/cancel", methods=["POST"])
def cancelPrint():
printer.cancelPrint()
return jsonify(SUCCESS)
@app.route(BASEURL + "control/temperature", methods=["POST"])
def setTargetTemperature():
if not printer.isOperational():
return jsonify(SUCCESS)
if request.values.has_key("temp"):
# set target temperature
temp = request.values["temp"];
printer.command("M104 S" + temp)
if request.values.has_key("bedTemp"):
# set target bed temperature
bedTemp = request.values["bedTemp"]
printer.command("M140 S" + bedTemp)
return jsonify(SUCCESS)
@app.route(BASEURL + "control/jog", methods=["POST"])
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)
if request.values.has_key("x"):
# jog x
x = request.values["x"]
printer.commands(["G91", "G1 X" + x + " F6000", "G90"])
if request.values.has_key("y"):
# jog y
y = request.values["y"]
printer.commands(["G91", "G1 Y" + y + " F6000", "G90"])
if request.values.has_key("z"):
# jog z
z = request.values["z"]
printer.commands(["G91", "G1 Z" + z + " F200", "G90"])
if request.values.has_key("homeXY"):
# home x/y
printer.command("G28 X0 Y0")
if request.values.has_key("homeZ"):
# home z
printer.command("G28 Z0")
return jsonify(SUCCESS)
@app.route(BASEURL + "control/speed", methods=["POST"])
def speed():
if not printer.isOperational():
return jsonify(SUCCESS)
for key in ["outerWall", "innerWall", "fill", "support"]:
if request.values.has_key(key):
value = int(request.values[key])
printer.setFeedrateModifier(key, value)
return jsonify(feedrate = printer.feedrateState())
#~~ GCODE file handling
@app.route(BASEURL + "gcodefiles", methods=["GET"])
def readGcodeFiles():
files = []
for osFile in os.listdir(UPLOAD_FOLDER):
if not fnmatch.fnmatch(osFile, "*.gcode"):
continue
files.append({
"name": osFile,
"size": sizeof_fmt(os.stat(os.path.join(UPLOAD_FOLDER, osFile)).st_size)
})
return jsonify(files=files)
@app.route(BASEURL + "gcodefiles/upload", methods=["POST"])
def uploadGcodeFile():
if request.files.has_key("gcode_file"):
file = request.files["gcode_file"]
if file and allowed_file(file.filename, set(["gcode"])):
secure = secure_filename(file.filename)
filename = os.path.join(UPLOAD_FOLDER, secure)
file.save(filename)
return readGcodeFiles()
@app.route(BASEURL + "gcodefiles/load", methods=["POST"])
def loadGcodeFile():
if request.values.has_key("filename"):
filename = request.values["filename"]
printer.loadGcode(os.path.join(UPLOAD_FOLDER, filename))
return jsonify(SUCCESS)
@app.route(BASEURL + "gcodefiles/delete", methods=["POST"])
def deleteGcodeFile():
if request.values.has_key("filename"):
filename = request.values["filename"]
if allowed_file(filename, set(["gcode"])):
secure = os.path.join(UPLOAD_FOLDER, secure_filename(filename))
if os.path.exists(secure):
os.remove(secure)
return readGcodeFiles()
#~~ timelapse handling
@app.route(BASEURL + "timelapse", methods=["GET"])
def getTimelapseData():
lapse = printer.getTimelapse()
type = "off"
additionalConfig = {}
if lapse is not None and isinstance(lapse, timelapse.ZTimelapse):
type = "zchange"
elif lapse is not None and isinstance(lapse, timelapse.TimedTimelapse):
type = "timed"
additionalConfig = {
"interval": lapse.interval
}
files = timelapse.getFinishedTimelapses()
for file in files:
file["size"] = sizeof_fmt(file["size"])
file["url"] = url_for("downloadTimelapse", filename=file["name"])
return jsonify({
"type": type,
"config": additionalConfig,
"files": files
})
@app.route(BASEURL + "timelapse/<filename>", methods=["GET"])
def downloadTimelapse(filename):
if allowed_file(filename, set(["mpg"])):
return send_from_directory(settings().getBaseFolder("timelapse"), filename, as_attachment=True)
@app.route(BASEURL + "timelapse/<filename>", methods=["DELETE"])
def deleteTimelapse(filename):
if allowed_file(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/config", methods=["POST"])
def setTimelapseConfig():
if request.values.has_key("type"):
type = request.values["type"]
lapse = None
if "zchange" == type:
lapse = timelapse.ZTimelapse()
elif "timed" == type:
interval = 10
if request.values.has_key("interval"):
try:
interval = int(request.values["interval"])
except ValueError:
pass
lapse = timelapse.TimedTimelapse(interval)
printer.setTimelapse(lapse)
return getTimelapseData()
#~~ settings
@app.route(BASEURL + "settings", methods=["GET"])
def getSettings():
s = settings()
return jsonify({
"serial_port": s.get("serial", "port"),
"serial_baudrate": s.get("serial", "baudrate")
})
@app.route(BASEURL + "settings", methods=["POST"])
def setSettings():
s = settings()
if request.values.has_key("serial_port"):
s.set("serial", "port", request.values["serial_port"])
if request.values.has_key("serial_baudrate"):
s.set("serial", "baudrate", request.values["serial_baudrate"])
s.save()
return getSettings()
#~~ helper functions
def sizeof_fmt(num):
"""
Taken from http://stackoverflow.com/questions/1094841/reusable-library-to-get-human-readable-version-of-file-size
"""
for x in ["bytes","KB","MB","GB"]:
if num < 1024.0:
return "%3.1f%s" % (num, x)
num /= 1024.0
return "%3.1f%s" % (num, "TB")
def allowed_file(filename, extensions):
return "." in filename and filename.rsplit(".", 1)[1] in extensions
#~~ startup code
def run(host = "0.0.0.0", port = 5000, debug = False):
app.debug = debug
app.run(host=host, port=port, use_reloader=False)
def main():
from optparse import OptionParser
defaultHost = settings().get("server", "host")
defaultPort = settings().get("server", "port")
parser = OptionParser(usage="usage: %prog [options]")
parser.add_option("-d", "--debug", action="store_true", dest="debug",
help="Enable debug mode")
parser.add_option("--host", action="store", type="string", default=defaultHost, dest="host",
help="Specify the host on which to bind the server, defaults to %s if not set" % (defaultHost))
parser.add_option("--port", action="store", type="int", default=defaultPort, dest="port",
help="Specify the port on which to bind the server, defaults to %s if not set" % (defaultPort))
(options, args) = parser.parse_args()
run(host=options.host, port=options.port, debug=options.debug)
if __name__ == "__main__":
main()