2012-12-25 10:55:00 +00:00
|
|
|
# coding=utf-8
|
2012-12-28 19:37:40 +00:00
|
|
|
__author__ = "Gina Häußge <osd@foosel.net>"
|
2012-12-31 12:18:54 +00:00
|
|
|
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
2012-12-25 10:55:00 +00:00
|
|
|
|
2013-01-04 12:11:00 +00:00
|
|
|
from flask import Flask, request, render_template, jsonify, send_from_directory, abort, url_for
|
2012-12-26 23:04:12 +00:00
|
|
|
from werkzeug import secure_filename
|
2013-01-06 15:51:04 +00:00
|
|
|
import tornadio2
|
2012-12-25 10:55:00 +00:00
|
|
|
|
2012-12-25 19:49:10 +00:00
|
|
|
import os
|
|
|
|
import fnmatch
|
2013-01-11 23:00:58 +00:00
|
|
|
import threading
|
2013-01-06 15:51:04 +00:00
|
|
|
|
2013-01-18 22:23:50 +00:00
|
|
|
from octoprint.printer import Printer, getConnectionOptions, PrinterCallback
|
|
|
|
from octoprint.settings import settings
|
|
|
|
import octoprint.timelapse as timelapse
|
2012-12-25 19:49:10 +00:00
|
|
|
|
2013-01-04 17:38:50 +00:00
|
|
|
BASEURL = "/ajax/"
|
|
|
|
SUCCESS = {}
|
2012-12-26 23:04:12 +00:00
|
|
|
|
2013-01-03 14:25:20 +00:00
|
|
|
UPLOAD_FOLDER = settings().getBaseFolder("uploads")
|
2012-12-25 10:55:00 +00:00
|
|
|
|
2013-01-18 22:23:50 +00:00
|
|
|
app = Flask("octoprint")
|
2012-12-25 10:55:00 +00:00
|
|
|
printer = Printer()
|
|
|
|
|
2012-12-28 19:37:40 +00:00
|
|
|
@app.route("/")
|
2012-12-25 10:55:00 +00:00
|
|
|
def index():
|
2013-01-04 17:38:50 +00:00
|
|
|
return render_template(
|
|
|
|
"index.html",
|
|
|
|
webcamStream=settings().get("webcam", "stream"),
|
2013-01-12 23:58:54 +00:00
|
|
|
enableTimelapse=(settings().get("webcam", "snapshot") is not None and settings().get("webcam", "ffmpeg") is not None),
|
|
|
|
enableEstimations=(settings().getBoolean("feature", "analyzeGcode"))
|
2013-01-04 17:38:50 +00:00
|
|
|
)
|
2012-12-25 10:55:00 +00:00
|
|
|
|
2012-12-25 19:49:10 +00:00
|
|
|
#~~ Printer state
|
2012-12-25 10:55:00 +00:00
|
|
|
|
2013-01-06 15:51:04 +00:00
|
|
|
class PrinterStateConnection(tornadio2.SocketConnection, PrinterCallback):
|
2013-01-11 23:00:58 +00:00
|
|
|
def __init__(self, session, endpoint=None):
|
|
|
|
tornadio2.SocketConnection.__init__(self, session, endpoint)
|
|
|
|
|
|
|
|
self._temperatureBacklog = []
|
|
|
|
self._temperatureBacklogMutex = threading.Lock()
|
|
|
|
self._logBacklog = []
|
|
|
|
self._logBacklogMutex = threading.Lock()
|
|
|
|
self._messageBacklog = []
|
|
|
|
self._messageBacklogMutex = threading.Lock()
|
|
|
|
|
2013-01-06 15:51:04 +00:00
|
|
|
def on_open(self, info):
|
2013-01-11 23:00:58 +00:00
|
|
|
print("New connection from client")
|
2013-01-06 15:51:04 +00:00
|
|
|
printer.registerCallback(self)
|
|
|
|
|
|
|
|
def on_close(self):
|
2013-01-11 23:00:58 +00:00
|
|
|
print("Closed client connection")
|
2013-01-06 15:51:04 +00:00
|
|
|
printer.unregisterCallback(self)
|
|
|
|
|
2013-01-06 21:10:59 +00:00
|
|
|
def on_message(self, message):
|
|
|
|
pass
|
|
|
|
|
2013-01-10 22:40:00 +00:00
|
|
|
def sendCurrentData(self, data):
|
2013-01-11 23:00:58 +00:00
|
|
|
# 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
|
|
|
|
})
|
2013-01-10 22:40:00 +00:00
|
|
|
self.emit("current", data)
|
|
|
|
|
|
|
|
def sendHistoryData(self, data):
|
|
|
|
self.emit("history", data)
|
2013-01-06 20:19:39 +00:00
|
|
|
|
2013-01-11 23:00:58 +00:00
|
|
|
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)
|
|
|
|
|
2012-12-25 19:49:10 +00:00
|
|
|
#~~ Printer control
|
|
|
|
|
2012-12-28 19:37:40 +00:00
|
|
|
@app.route(BASEURL + "control/connectionOptions", methods=["GET"])
|
|
|
|
def connectionOptions():
|
|
|
|
return jsonify(getConnectionOptions())
|
2012-12-25 10:55:00 +00:00
|
|
|
|
2012-12-28 19:37:40 +00:00
|
|
|
@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"]
|
2013-01-01 20:04:00 +00:00
|
|
|
if request.values.has_key("save"):
|
|
|
|
settings().set("serial", "port", port)
|
|
|
|
settings().set("serial", "baudrate", baudrate)
|
|
|
|
settings().save()
|
2012-12-28 19:37:40 +00:00
|
|
|
printer.connect(port=port, baudrate=baudrate)
|
|
|
|
return jsonify(state="Connecting")
|
|
|
|
|
|
|
|
@app.route(BASEURL + "control/disconnect", methods=["POST"])
|
2012-12-25 19:49:10 +00:00
|
|
|
def disconnect():
|
|
|
|
printer.disconnect()
|
2012-12-28 19:37:40 +00:00
|
|
|
return jsonify(state="Offline")
|
2012-12-25 19:49:10 +00:00
|
|
|
|
2012-12-28 19:37:40 +00:00
|
|
|
@app.route(BASEURL + "control/command", methods=["POST"])
|
2012-12-25 19:49:10 +00:00
|
|
|
def printerCommand():
|
2012-12-28 19:37:40 +00:00
|
|
|
command = request.form["command"]
|
2012-12-25 19:49:10 +00:00
|
|
|
printer.command(command)
|
|
|
|
return jsonify(SUCCESS)
|
|
|
|
|
2012-12-28 19:37:40 +00:00
|
|
|
@app.route(BASEURL + "control/print", methods=["POST"])
|
2012-12-25 10:55:00 +00:00
|
|
|
def printGcode():
|
|
|
|
printer.startPrint()
|
2012-12-25 19:49:10 +00:00
|
|
|
return jsonify(SUCCESS)
|
2012-12-25 10:55:00 +00:00
|
|
|
|
2012-12-28 19:37:40 +00:00
|
|
|
@app.route(BASEURL + "control/pause", methods=["POST"])
|
2012-12-25 10:55:00 +00:00
|
|
|
def pausePrint():
|
|
|
|
printer.togglePausePrint()
|
2012-12-25 19:49:10 +00:00
|
|
|
return jsonify(SUCCESS)
|
2012-12-25 10:55:00 +00:00
|
|
|
|
2012-12-28 19:37:40 +00:00
|
|
|
@app.route(BASEURL + "control/cancel", methods=["POST"])
|
2012-12-25 10:55:00 +00:00
|
|
|
def cancelPrint():
|
|
|
|
printer.cancelPrint()
|
2012-12-25 19:49:10 +00:00
|
|
|
return jsonify(SUCCESS)
|
|
|
|
|
2012-12-28 19:37:40 +00:00
|
|
|
@app.route(BASEURL + "control/temperature", methods=["POST"])
|
2012-12-26 14:03:34 +00:00
|
|
|
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():
|
2012-12-28 19:37:40 +00:00
|
|
|
# do not jog when a print job is running or we don"t have a connection
|
2012-12-26 14:03:34 +00:00
|
|
|
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)
|
|
|
|
|
2012-12-29 14:41:23 +00:00
|
|
|
@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())
|
|
|
|
|
2012-12-25 19:49:10 +00:00
|
|
|
#~~ GCODE file handling
|
|
|
|
|
2012-12-28 19:37:40 +00:00
|
|
|
@app.route(BASEURL + "gcodefiles", methods=["GET"])
|
2012-12-25 19:49:10 +00:00
|
|
|
def readGcodeFiles():
|
|
|
|
files = []
|
|
|
|
for osFile in os.listdir(UPLOAD_FOLDER):
|
|
|
|
if not fnmatch.fnmatch(osFile, "*.gcode"):
|
|
|
|
continue
|
|
|
|
files.append({
|
|
|
|
"name": osFile,
|
2012-12-29 14:41:23 +00:00
|
|
|
"size": sizeof_fmt(os.stat(os.path.join(UPLOAD_FOLDER, osFile)).st_size)
|
2012-12-25 19:49:10 +00:00
|
|
|
})
|
|
|
|
return jsonify(files=files)
|
|
|
|
|
2012-12-28 19:37:40 +00:00
|
|
|
@app.route(BASEURL + "gcodefiles/upload", methods=["POST"])
|
2012-12-25 19:49:10 +00:00
|
|
|
def uploadGcodeFile():
|
2013-01-04 12:11:00 +00:00
|
|
|
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)
|
2012-12-25 19:49:10 +00:00
|
|
|
return readGcodeFiles()
|
|
|
|
|
2012-12-28 19:37:40 +00:00
|
|
|
@app.route(BASEURL + "gcodefiles/load", methods=["POST"])
|
2012-12-25 19:49:10 +00:00
|
|
|
def loadGcodeFile():
|
2013-01-04 12:11:00 +00:00
|
|
|
if request.values.has_key("filename"):
|
|
|
|
filename = request.values["filename"]
|
|
|
|
printer.loadGcode(os.path.join(UPLOAD_FOLDER, filename))
|
2012-12-25 19:49:10 +00:00
|
|
|
return jsonify(SUCCESS)
|
|
|
|
|
2012-12-28 19:37:40 +00:00
|
|
|
@app.route(BASEURL + "gcodefiles/delete", methods=["POST"])
|
2012-12-25 19:49:10 +00:00
|
|
|
def deleteGcodeFile():
|
2012-12-26 23:04:12 +00:00
|
|
|
if request.values.has_key("filename"):
|
|
|
|
filename = request.values["filename"]
|
2013-01-04 12:11:00 +00:00
|
|
|
if allowed_file(filename, set(["gcode"])):
|
2012-12-29 14:41:23 +00:00
|
|
|
secure = os.path.join(UPLOAD_FOLDER, secure_filename(filename))
|
2012-12-26 23:04:12 +00:00
|
|
|
if os.path.exists(secure):
|
|
|
|
os.remove(secure)
|
2012-12-25 19:49:10 +00:00
|
|
|
return readGcodeFiles()
|
|
|
|
|
2013-01-04 12:11:00 +00:00
|
|
|
#~~ timelapse handling
|
2013-01-03 14:25:20 +00:00
|
|
|
|
|
|
|
@app.route(BASEURL + "timelapse", methods=["GET"])
|
2013-01-04 12:11:00 +00:00
|
|
|
def getTimelapseData():
|
|
|
|
lapse = printer.getTimelapse()
|
2013-01-03 14:25:20 +00:00
|
|
|
|
|
|
|
type = "off"
|
|
|
|
additionalConfig = {}
|
2013-01-04 12:11:00 +00:00
|
|
|
if lapse is not None and isinstance(lapse, timelapse.ZTimelapse):
|
2013-01-03 14:25:20 +00:00
|
|
|
type = "zchange"
|
2013-01-04 12:11:00 +00:00
|
|
|
elif lapse is not None and isinstance(lapse, timelapse.TimedTimelapse):
|
2013-01-03 14:25:20 +00:00
|
|
|
type = "timed"
|
|
|
|
additionalConfig = {
|
2013-01-04 12:11:00 +00:00
|
|
|
"interval": lapse.interval
|
2013-01-03 14:25:20 +00:00
|
|
|
}
|
|
|
|
|
2013-01-04 12:11:00 +00:00
|
|
|
files = timelapse.getFinishedTimelapses()
|
|
|
|
for file in files:
|
|
|
|
file["size"] = sizeof_fmt(file["size"])
|
|
|
|
file["url"] = url_for("downloadTimelapse", filename=file["name"])
|
|
|
|
|
2013-01-03 14:25:20 +00:00
|
|
|
return jsonify({
|
2013-01-04 12:11:00 +00:00
|
|
|
"type": type,
|
|
|
|
"config": additionalConfig,
|
|
|
|
"files": files
|
2013-01-03 14:25:20 +00:00
|
|
|
})
|
|
|
|
|
2013-01-04 12:11:00 +00:00
|
|
|
@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"])
|
2013-01-03 14:25:20 +00:00
|
|
|
def setTimelapseConfig():
|
2013-01-04 12:11:00 +00:00
|
|
|
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()
|
2013-01-03 14:25:20 +00:00
|
|
|
|
2013-01-01 20:04:00 +00:00
|
|
|
#~~ 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
|
|
|
|
|
2012-12-25 19:49:10 +00:00
|
|
|
def sizeof_fmt(num):
|
|
|
|
"""
|
|
|
|
Taken from http://stackoverflow.com/questions/1094841/reusable-library-to-get-human-readable-version-of-file-size
|
|
|
|
"""
|
2012-12-28 19:37:40 +00:00
|
|
|
for x in ["bytes","KB","MB","GB"]:
|
2012-12-25 19:49:10 +00:00
|
|
|
if num < 1024.0:
|
|
|
|
return "%3.1f%s" % (num, x)
|
|
|
|
num /= 1024.0
|
2012-12-28 19:37:40 +00:00
|
|
|
return "%3.1f%s" % (num, "TB")
|
2012-12-25 10:55:00 +00:00
|
|
|
|
2013-01-04 12:11:00 +00:00
|
|
|
def allowed_file(filename, extensions):
|
|
|
|
return "." in filename and filename.rsplit(".", 1)[1] in extensions
|
2012-12-26 23:04:12 +00:00
|
|
|
|
2013-01-01 20:04:00 +00:00
|
|
|
#~~ startup code
|
|
|
|
|
2012-12-29 11:11:49 +00:00
|
|
|
def run(host = "0.0.0.0", port = 5000, debug = False):
|
2013-01-06 15:51:04 +00:00
|
|
|
from tornado.wsgi import WSGIContainer
|
|
|
|
from tornado.httpserver import HTTPServer
|
|
|
|
from tornado.ioloop import IOLoop
|
|
|
|
from tornado.web import Application, FallbackHandler
|
|
|
|
|
|
|
|
print "Listening on http://%s:%d" % (host, port)
|
2012-12-29 11:11:49 +00:00
|
|
|
app.debug = debug
|
2013-01-06 15:51:04 +00:00
|
|
|
|
|
|
|
router = tornadio2.TornadioRouter(PrinterStateConnection)
|
|
|
|
tornado_app = Application(router.urls + [
|
|
|
|
(".*", FallbackHandler, {"fallback": WSGIContainer(app)})
|
|
|
|
])
|
|
|
|
server = HTTPServer(tornado_app)
|
|
|
|
server.listen(port, address=host)
|
|
|
|
IOLoop.instance().start()
|
2012-12-31 12:18:54 +00:00
|
|
|
|
|
|
|
def main():
|
|
|
|
from optparse import OptionParser
|
|
|
|
|
2013-01-01 20:04:00 +00:00
|
|
|
defaultHost = settings().get("server", "host")
|
|
|
|
defaultPort = settings().get("server", "port")
|
|
|
|
|
2012-12-31 12:18:54 +00:00
|
|
|
parser = OptionParser(usage="usage: %prog [options]")
|
|
|
|
parser.add_option("-d", "--debug", action="store_true", dest="debug",
|
|
|
|
help="Enable debug mode")
|
2013-01-01 20:04:00 +00:00
|
|
|
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))
|
2012-12-31 12:18:54 +00:00
|
|
|
(options, args) = parser.parse_args()
|
|
|
|
|
|
|
|
run(host=options.host, port=options.port, debug=options.debug)
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
main()
|