- Gcode for printjob is now loaded asynchronously, including a progress indicator in the frontend
- New connection section which also allows selection of port and baudrate to use for connecting to printer (default values from profile file are pre-selected if available) - Connect button now switches to disconnect-functionality while connected - Bugfix: Jog controls are now also enabled if no job is loaded - Cleanups: More consistent quotes, some documentationmaster
parent
083498865d
commit
f40460d797
|
@ -1,11 +1,11 @@
|
|||
#!/usr/bin/env python
|
||||
# coding=utf-8
|
||||
__author__ = 'Gina Häußge <osd@foosel.net>'
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
|
||||
from flask import Flask, request, render_template, jsonify, make_response
|
||||
from werkzeug import secure_filename
|
||||
|
||||
from printer import Printer
|
||||
from printer import Printer, getConnectionOptions
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
@ -16,15 +16,15 @@ BASEURL="/ajax/"
|
|||
SUCCESS={}
|
||||
|
||||
# taken from http://stackoverflow.com/questions/1084697/how-do-i-store-desktop-application-data-in-a-cross-platform-way-for-python
|
||||
if sys.platform == 'darwin':
|
||||
if sys.platform == "darwin":
|
||||
from AppKit import NSSearchPathForDirectoriesInDomains
|
||||
# http://developer.apple.com/DOCUMENTATION/Cocoa/Reference/Foundation/Miscellaneous/Foundation_Functions/Reference/reference.html#//apple_ref/c/func/NSSearchPathForDirectoriesInDomains
|
||||
# NSApplicationSupportDirectory = 14
|
||||
# NSUserDomainMask = 1
|
||||
# True for expanding the tilde into a fully qualified path
|
||||
appdata = os.path.join(NSSearchPathForDirectoriesInDomains(14, 1, True)[0], APPNAME)
|
||||
elif sys.platform == 'win32':
|
||||
appdata = os.path.join(os.environ['APPDATA'], APPNAME)
|
||||
elif sys.platform == "win32":
|
||||
appdata = os.path.join(os.environ["APPDATA"], APPNAME)
|
||||
else:
|
||||
appdata = os.path.expanduser(os.path.join("~", "." + APPNAME.lower()))
|
||||
|
||||
|
@ -36,94 +36,111 @@ ALLOWED_EXTENSIONS = set(["gcode"])
|
|||
app = Flask("Cura.webui")
|
||||
printer = Printer()
|
||||
|
||||
@app.route('/')
|
||||
@app.route("/")
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
return render_template("index.html")
|
||||
|
||||
#~~ Printer state
|
||||
|
||||
@app.route(BASEURL + 'state', methods=['GET'])
|
||||
@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()
|
||||
|
||||
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()
|
||||
"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 != None):
|
||||
result['job'] = jobData
|
||||
if jobData is not None:
|
||||
jobData["filename"] = jobData["filename"].replace(UPLOAD_FOLDER + os.sep, "")
|
||||
result["job"] = jobData
|
||||
|
||||
if (request.values.has_key('temperatures')):
|
||||
result['temperatures'] = printer.temps
|
||||
if gcodeState is not None:
|
||||
gcodeState["filename"] = gcodeState["filename"].replace(UPLOAD_FOLDER + os.sep, "")
|
||||
result["gcode"] = gcodeState
|
||||
|
||||
if (request.values.has_key('log')):
|
||||
result['log'] = printer.log
|
||||
if request.values.has_key("temperatures"):
|
||||
result["temperatures"] = printer.temps
|
||||
|
||||
if (request.values.has_key('messages')):
|
||||
result['messages'] = printer.messages
|
||||
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'])
|
||||
@app.route(BASEURL + "state/messages", methods=["GET"])
|
||||
def printerMessages():
|
||||
return jsonify(messages=printer.messages)
|
||||
|
||||
@app.route(BASEURL + 'state/log', methods=['GET'])
|
||||
@app.route(BASEURL + "state/log", methods=["GET"])
|
||||
def printerLogs():
|
||||
return jsonify(log=printer.log)
|
||||
|
||||
@app.route(BASEURL + 'state/temperatures', methods=['GET'])
|
||||
@app.route(BASEURL + "state/temperatures", methods=["GET"])
|
||||
def printerTemperatures():
|
||||
return jsonify(temperatures = printer.temps)
|
||||
|
||||
#~~ Printer control
|
||||
|
||||
@app.route(BASEURL + 'control/connect', methods=['POST'])
|
||||
def connect():
|
||||
printer.connect()
|
||||
return jsonify(state='Connecting')
|
||||
@app.route(BASEURL + "control/connectionOptions", methods=["GET"])
|
||||
def connectionOptions():
|
||||
return jsonify(getConnectionOptions())
|
||||
|
||||
@app.route(BASEURL + 'control/disconnect', methods=['POST'])
|
||||
@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"]
|
||||
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')
|
||||
return jsonify(state="Offline")
|
||||
|
||||
@app.route(BASEURL + 'control/command', methods=['POST'])
|
||||
@app.route(BASEURL + "control/command", methods=["POST"])
|
||||
def printerCommand():
|
||||
command = request.form['command']
|
||||
command = request.form["command"]
|
||||
printer.command(command)
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
@app.route(BASEURL + 'control/print', methods=['POST'])
|
||||
@app.route(BASEURL + "control/print", methods=["POST"])
|
||||
def printGcode():
|
||||
printer.startPrint()
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
@app.route(BASEURL + 'control/pause', methods=['POST'])
|
||||
@app.route(BASEURL + "control/pause", methods=["POST"])
|
||||
def pausePrint():
|
||||
printer.togglePausePrint()
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
@app.route(BASEURL + 'control/cancel', methods=['POST'])
|
||||
@app.route(BASEURL + "control/cancel", methods=["POST"])
|
||||
def cancelPrint():
|
||||
printer.cancelPrint()
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
@app.route(BASEURL + 'control/temperature', methods=['POST'])
|
||||
@app.route(BASEURL + "control/temperature", methods=["POST"])
|
||||
def setTargetTemperature():
|
||||
if not printer.isOperational():
|
||||
return jsonify(SUCCESS)
|
||||
|
@ -143,7 +160,7 @@ def setTargetTemperature():
|
|||
@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
|
||||
# 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"):
|
||||
|
@ -169,7 +186,7 @@ def jog():
|
|||
|
||||
#~~ GCODE file handling
|
||||
|
||||
@app.route(BASEURL + 'gcodefiles', methods=['GET'])
|
||||
@app.route(BASEURL + "gcodefiles", methods=["GET"])
|
||||
def readGcodeFiles():
|
||||
files = []
|
||||
for osFile in os.listdir(UPLOAD_FOLDER):
|
||||
|
@ -181,22 +198,22 @@ def readGcodeFiles():
|
|||
})
|
||||
return jsonify(files=files)
|
||||
|
||||
@app.route(BASEURL + 'gcodefiles/upload', methods=['POST'])
|
||||
@app.route(BASEURL + "gcodefiles/upload", methods=["POST"])
|
||||
def uploadGcodeFile():
|
||||
file = request.files['gcode_file']
|
||||
file = request.files["gcode_file"]
|
||||
if file and allowed_file(file.filename):
|
||||
secure = secure_filename(file.filename)
|
||||
filename = os.path.join(UPLOAD_FOLDER, secure)
|
||||
file.save(filename)
|
||||
return readGcodeFiles()
|
||||
|
||||
@app.route(BASEURL + 'gcodefiles/load', methods=['POST'])
|
||||
@app.route(BASEURL + "gcodefiles/load", methods=["POST"])
|
||||
def loadGcodeFile():
|
||||
filename = request.values["filename"]
|
||||
printer.loadGcode(UPLOAD_FOLDER + os.sep + filename)
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
@app.route(BASEURL + 'gcodefiles/delete', methods=['POST'])
|
||||
@app.route(BASEURL + "gcodefiles/delete", methods=["POST"])
|
||||
def deleteGcodeFile():
|
||||
if request.values.has_key("filename"):
|
||||
filename = request.values["filename"]
|
||||
|
@ -210,14 +227,15 @@ 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']:
|
||||
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')
|
||||
return "%3.1f%s" % (num, "TB")
|
||||
|
||||
def allowed_file(filename):
|
||||
return "." in filename and filename.rsplit(".", 1)[1] in ALLOWED_EXTENSIONS
|
||||
|
||||
def run():
|
||||
app.debug = True
|
||||
app.run(host="0.0.0.0", port=5000)
|
||||
|
|
|
@ -1,20 +1,33 @@
|
|||
# coding=utf-8
|
||||
__author__ = 'Gina Häußge <osd@foosel.net>'
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
|
||||
import time
|
||||
import os
|
||||
from threading import Thread
|
||||
|
||||
import Cura.util.machineCom as machineCom
|
||||
from Cura.util import gcodeInterpreter
|
||||
from Cura.util import profile
|
||||
|
||||
def getConnectionOptions():
|
||||
"""
|
||||
Retrieves the available ports, baudrates, prefered port and baudrate for connecting to the printer.
|
||||
"""
|
||||
return {
|
||||
"ports": sorted(machineCom.serialList(), key=str.lower),
|
||||
"baudrates": sorted(machineCom.baudrateList(), key=int, reverse=True),
|
||||
"portPreference": profile.getPreference('serial_port_auto'),
|
||||
"baudratePreference": int(profile.getPreference('serial_baud_auto'))
|
||||
}
|
||||
|
||||
class Printer():
|
||||
def __init__(self):
|
||||
# state
|
||||
self.temps = {
|
||||
'actual': [],
|
||||
'target': [],
|
||||
'actualBed': [],
|
||||
'targetBed': []
|
||||
"actual": [],
|
||||
"target": [],
|
||||
"actualBed": [],
|
||||
"targetBed": []
|
||||
}
|
||||
self.messages = []
|
||||
self.log = []
|
||||
|
@ -32,44 +45,68 @@ class Printer():
|
|||
self.gcodeList = None
|
||||
self.filename = None
|
||||
|
||||
self.gcodeLoader = None
|
||||
|
||||
# comm
|
||||
self.comm = None
|
||||
|
||||
def connect(self):
|
||||
if self.comm != None:
|
||||
def connect(self, port=None, baudrate=None):
|
||||
"""
|
||||
Connects to the printer. If port and/or baudrate is provided, uses these settings, otherwise autodetection
|
||||
will be attempted.
|
||||
"""
|
||||
if self.comm is not None:
|
||||
self.comm.close()
|
||||
self.comm = machineCom.MachineCom(callbackObject=self)
|
||||
self.comm = machineCom.MachineCom(port, baudrate, callbackObject=self)
|
||||
|
||||
def disconnect(self):
|
||||
if self.comm != None:
|
||||
"""
|
||||
Closes the connection to the printer.
|
||||
"""
|
||||
if self.comm is not None:
|
||||
self.comm.close()
|
||||
self.comm = None
|
||||
|
||||
def command(self, command):
|
||||
"""
|
||||
Sends a single gcode command to the printer.
|
||||
"""
|
||||
self.commands([command])
|
||||
|
||||
def commands(self, commands):
|
||||
"""
|
||||
Sends multiple gcode commands (provided as a list) to the printer.
|
||||
"""
|
||||
for command in commands:
|
||||
self.comm.sendCommand(command)
|
||||
|
||||
def mcLog(self, message):
|
||||
"""
|
||||
Callback method for the comm object, called upon log output.
|
||||
Log line is stored in internal buffer, which is truncated to the last 300 lines.
|
||||
"""
|
||||
self.log.append(message)
|
||||
self.log = self.log[-300:]
|
||||
|
||||
def mcTempUpdate(self, temp, bedTemp, targetTemp, bedTargetTemp):
|
||||
"""
|
||||
Callback method for the comm object, called upon receiving new temperature information.
|
||||
Temperature information (actual and target) for print head and print bed is stored in corresponding
|
||||
temperature history (including timestamp), history is truncated to 300 entries.
|
||||
"""
|
||||
currentTime = int(time.time() * 1000)
|
||||
|
||||
self.temps['actual'].append((currentTime, temp))
|
||||
self.temps['actual'] = self.temps['actual'][-300:]
|
||||
self.temps["actual"].append((currentTime, temp))
|
||||
self.temps["actual"] = self.temps["actual"][-300:]
|
||||
|
||||
self.temps['target'].append((currentTime, targetTemp))
|
||||
self.temps['target'] = self.temps['target'][-300:]
|
||||
self.temps["target"].append((currentTime, targetTemp))
|
||||
self.temps["target"] = self.temps["target"][-300:]
|
||||
|
||||
self.temps['actualBed'].append((currentTime, bedTemp))
|
||||
self.temps['actualBed'] = self.temps['actualBed'][-300:]
|
||||
self.temps["actualBed"].append((currentTime, bedTemp))
|
||||
self.temps["actualBed"] = self.temps["actualBed"][-300:]
|
||||
|
||||
self.temps['targetBed'].append((currentTime, bedTargetTemp))
|
||||
self.temps['targetBed'] = self.temps['targetBed'][-300:]
|
||||
self.temps["targetBed"].append((currentTime, bedTargetTemp))
|
||||
self.temps["targetBed"] = self.temps["targetBed"][-300:]
|
||||
|
||||
self.currentTemp = temp
|
||||
self.currentTargetTemp = targetTemp
|
||||
|
@ -77,22 +114,55 @@ class Printer():
|
|||
self.currentBedTargetTemp = bedTargetTemp
|
||||
|
||||
def mcStateChange(self, state):
|
||||
"""
|
||||
Callback method for the comm object, called if the connection state changes.
|
||||
New state is stored for retrieval by the frontend.
|
||||
"""
|
||||
self.state = state
|
||||
|
||||
def mcMessage(self, message):
|
||||
"""
|
||||
Callback method for the comm object, called upon message exchanges via serial.
|
||||
Stores the message in the message buffer, truncates buffer to the last 300 lines.
|
||||
"""
|
||||
self.messages.append(message)
|
||||
self.messages = self.messages[-300:]
|
||||
|
||||
def mcProgress(self, lineNr):
|
||||
"""
|
||||
Callback method for the comm object, called upon any change in progress of the printjob.
|
||||
Triggers storage of new values for printTime, printTimeLeft and the current line.
|
||||
"""
|
||||
self.printTime = self.comm.getPrintTime()
|
||||
self.printTimeLeft = self.comm.getPrintTimeRemainingEstimate()
|
||||
self.progress = self.comm.getPrintPos()
|
||||
|
||||
def mcZChange(self, newZ):
|
||||
"""
|
||||
Callback method for the comm object, called upon change of the z-layer.
|
||||
"""
|
||||
self.currentZ = newZ
|
||||
|
||||
def onGcodeLoaded(self, gcodeLoader):
|
||||
"""
|
||||
Callback method for the gcode loader, gets called when the gcode for the new printjob has finished loading.
|
||||
Takes care to set filename, gcode and commandlist from the gcode loader and reset print job progress.
|
||||
"""
|
||||
self.filename = gcodeLoader.filename
|
||||
self.gcode = gcodeLoader.gcode
|
||||
self.gcodeList = gcodeLoader.gcodeList
|
||||
self.currentZ = None
|
||||
self.progress = None
|
||||
self.printTime = None
|
||||
self.printTimeLeft = None
|
||||
|
||||
self.gcodeLoader = None
|
||||
|
||||
def jobData(self):
|
||||
if self.gcode != None:
|
||||
"""
|
||||
Returns statistics regarding the currently loaded printjob, or None if no printjob is loaded.
|
||||
"""
|
||||
if self.gcode is not None:
|
||||
formattedPrintTime = None
|
||||
if (self.printTime):
|
||||
formattedPrintTime = "%02d:%02d" % (int(self.printTime / 60), int(self.printTime % 60))
|
||||
|
@ -102,16 +172,17 @@ class Printer():
|
|||
formattedPrintTimeLeft = "%02d:%02d" % (int(self.printTimeLeft / 60), int(self.printTimeLeft % 60))
|
||||
|
||||
data = {
|
||||
'currentZ': self.currentZ,
|
||||
'line': self.progress,
|
||||
'totalLines': len(self.gcodeList),
|
||||
'printTime': formattedPrintTime,
|
||||
'printTimeLeft': formattedPrintTimeLeft,
|
||||
'filament': "%.2fm %.2fg" % (
|
||||
"filename": self.filename,
|
||||
"currentZ": self.currentZ,
|
||||
"line": self.progress,
|
||||
"totalLines": len(self.gcodeList),
|
||||
"printTime": formattedPrintTime,
|
||||
"printTimeLeft": formattedPrintTimeLeft,
|
||||
"filament": "%.2fm %.2fg" % (
|
||||
self.gcode.extrusionAmount / 1000,
|
||||
self.gcode.calculateWeight() * 1000
|
||||
),
|
||||
'estimatedPrintTime': "%02d:%02d" % (
|
||||
"estimatedPrintTime": "%02d:%02d" % (
|
||||
int(self.gcode.totalMoveTimeMinute / 60),
|
||||
int(self.gcode.totalMoveTimeMinute % 60)
|
||||
)
|
||||
|
@ -120,64 +191,68 @@ class Printer():
|
|||
data = None
|
||||
return data
|
||||
|
||||
def gcodeState(self):
|
||||
if self.gcodeLoader is not None:
|
||||
return {
|
||||
"filename": self.gcodeLoader.filename,
|
||||
"progress": self.gcodeLoader.progress
|
||||
}
|
||||
else:
|
||||
return None
|
||||
|
||||
def getStateString(self):
|
||||
if self.comm == None:
|
||||
return 'Offline'
|
||||
"""
|
||||
Returns a human readable string corresponding to the current communication state.
|
||||
"""
|
||||
if self.comm is None:
|
||||
return "Offline"
|
||||
else:
|
||||
return self.comm.getStateString()
|
||||
|
||||
def isClosedOrError(self):
|
||||
return self.comm == None or self.comm.isClosedOrError()
|
||||
return self.comm is None or self.comm.isClosedOrError()
|
||||
|
||||
def isOperational(self):
|
||||
return self.comm != None and self.comm.isOperational()
|
||||
return self.comm is not None and self.comm.isOperational()
|
||||
|
||||
def isPrinting(self):
|
||||
return self.comm != None and self.comm.isPrinting()
|
||||
return self.comm is not None and self.comm.isPrinting()
|
||||
|
||||
def isPaused(self):
|
||||
return self.comm != None and self.comm.isPaused()
|
||||
return self.comm is not None and self.comm.isPaused()
|
||||
|
||||
def isError(self):
|
||||
return self.comm != None and self.comm.isError()
|
||||
return self.comm is not None and self.comm.isError()
|
||||
|
||||
def isReady(self):
|
||||
return self.gcodeList and len(self.gcodeList) > 0
|
||||
return self.gcodeLoader is None and self.gcodeList and len(self.gcodeList) > 0
|
||||
|
||||
def isLoading(self):
|
||||
return self.gcodeLoader is not None
|
||||
|
||||
def loadGcode(self, file):
|
||||
if self.comm != None and self.comm.isPrinting():
|
||||
"""
|
||||
Loads the gcode from the given file as the new print job.
|
||||
Aborts if the printer is currently printing or another gcode file is currently being loaded.
|
||||
"""
|
||||
if (self.comm is not None and self.comm.isPrinting()) or (self.gcodeLoader is not None):
|
||||
return
|
||||
|
||||
#Send an initial M110 to reset the line counter to zero.
|
||||
prevLineType = lineType = 'CUSTOM'
|
||||
gcodeList = ["M110"]
|
||||
for line in open(file, 'r'):
|
||||
if line.startswith(';TYPE:'):
|
||||
lineType = line[6:].strip()
|
||||
if ';' in line:
|
||||
line = line[0:line.find(';')]
|
||||
line = line.strip()
|
||||
if len(line) > 0:
|
||||
if prevLineType != lineType:
|
||||
gcodeList.append((line, lineType, ))
|
||||
else:
|
||||
gcodeList.append(line)
|
||||
prevLineType = lineType
|
||||
gcode = gcodeInterpreter.gcode()
|
||||
gcode.loadList(gcodeList)
|
||||
#print "Loaded: %s (%d)" % (filename, len(gcodeList))
|
||||
self.filename = file
|
||||
self.gcode = gcode
|
||||
self.gcodeList = gcodeList
|
||||
self.currentZ = None
|
||||
self.progress = None
|
||||
self.printTime = None
|
||||
self.printTimeLeft = None
|
||||
self.filename = None
|
||||
self.gcode = None
|
||||
self.gcodeList = None
|
||||
|
||||
self.gcodeLoader = GcodeLoader(file, self)
|
||||
self.gcodeLoader.start()
|
||||
|
||||
def startPrint(self):
|
||||
if self.comm == None or not self.comm.isOperational():
|
||||
"""
|
||||
Starts the currently loaded print job.
|
||||
Only starts if the printer is connected and operational, not currently printing and a printjob is loaded
|
||||
"""
|
||||
if self.comm is None or not self.comm.isOperational():
|
||||
return
|
||||
if self.gcodeList == None:
|
||||
if self.gcodeList is None:
|
||||
return
|
||||
if self.comm.isPrinting():
|
||||
return
|
||||
|
@ -185,13 +260,72 @@ class Printer():
|
|||
self.comm.printGCode(self.gcodeList)
|
||||
|
||||
def togglePausePrint(self):
|
||||
if self.comm == None:
|
||||
"""
|
||||
Pause the current printjob.
|
||||
"""
|
||||
if self.comm is None:
|
||||
return
|
||||
self.comm.setPause(not self.comm.isPaused())
|
||||
|
||||
def cancelPrint(self):
|
||||
if self.comm == None:
|
||||
def cancelPrint(self, disableMotorsAndHeater=True):
|
||||
"""
|
||||
Cancel the current printjob.
|
||||
"""
|
||||
if self.comm is None:
|
||||
return
|
||||
self.comm.cancelPrint()
|
||||
self.comm.sendCommands(["M84", "M104 S0", "M140 S0"]) # disable motors, switch off heaters
|
||||
if disableMotorsAndHeater:
|
||||
self.commands(["M84", "M104 S0", "M140 S0"]) # disable motors, switch off heaters
|
||||
|
||||
# reset line, height, print time
|
||||
self.currentZ = None
|
||||
self.progress = None
|
||||
self.printTime = None
|
||||
self.printTimeLeft = None
|
||||
|
||||
class GcodeLoader(Thread):
|
||||
"""
|
||||
The GcodeLoader takes care of loading a gcode-File from disk and parsing it into a gcode object in a separate
|
||||
thread while constantly notifying interested listeners about the current progress.
|
||||
The progress is returned as a float value between 0 and 1 which is to be interpreted as the percentage of completion.
|
||||
"""
|
||||
|
||||
def __init__(self, filename, printerCallback):
|
||||
Thread.__init__(self);
|
||||
|
||||
self.printerCallback = printerCallback;
|
||||
|
||||
self.filename = filename
|
||||
self.progress = None
|
||||
|
||||
self.gcode = None
|
||||
self.gcodeList = None
|
||||
|
||||
def run(self):
|
||||
#Send an initial M110 to reset the line counter to zero.
|
||||
prevLineType = lineType = "CUSTOM"
|
||||
gcodeList = ["M110"]
|
||||
with open(self.filename, "r") as file:
|
||||
for line in file:
|
||||
if line.startswith(";TYPE:"):
|
||||
lineType = line[6:].strip()
|
||||
if ";" in line:
|
||||
line = line[0:line.find(";")]
|
||||
line = line.strip()
|
||||
if len(line) > 0:
|
||||
if prevLineType != lineType:
|
||||
gcodeList.append((line, lineType, ))
|
||||
else:
|
||||
gcodeList.append(line)
|
||||
prevLineType = lineType
|
||||
|
||||
self.gcodeList = gcodeList
|
||||
self.gcode = gcodeInterpreter.gcode()
|
||||
self.gcode.progressCallback = self.onProgress
|
||||
self.gcode.loadList(self.gcodeList)
|
||||
|
||||
self.printerCallback.onGcodeLoaded(self)
|
||||
|
||||
def onProgress(self, progress):
|
||||
self.progress = progress
|
||||
|
||||
|
|
|
@ -50,4 +50,8 @@ table th.gcode_files_action, table td.gcode_files_action {
|
|||
|
||||
#temp_newTemp {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#connection_ports, #connection_baudrates {
|
||||
width: 100%;
|
||||
}
|
|
@ -1,3 +1,65 @@
|
|||
function ConnectionViewModel() {
|
||||
var self = this;
|
||||
|
||||
self.portOptions = ko.observableArray(undefined);
|
||||
self.baudrateOptions = ko.observableArray(undefined);
|
||||
self.selectedPort = ko.observable(undefined);
|
||||
self.selectedBaudrate = ko.observable(undefined);
|
||||
|
||||
self.isErrorOrClosed = ko.observable(undefined);
|
||||
self.isOperational = ko.observable(undefined);
|
||||
self.isPrinting = ko.observable(undefined);
|
||||
self.isPaused = ko.observable(undefined);
|
||||
self.isError = ko.observable(undefined);
|
||||
self.isReady = ko.observable(undefined);
|
||||
self.isLoading = ko.observable(undefined);
|
||||
|
||||
self.buttonText = ko.computed(function() {
|
||||
if (self.isErrorOrClosed())
|
||||
return "Connect";
|
||||
else
|
||||
return "Disconnect";
|
||||
})
|
||||
|
||||
self.fromResponse = function(response) {
|
||||
self.portOptions(response.ports);
|
||||
self.baudrateOptions(response.baudrates);
|
||||
|
||||
if (!self.selectedPort() && response.ports && response.ports.indexOf(response.portPreference) >= 0)
|
||||
self.selectedPort(response.portPreference);
|
||||
if (!self.selectedBaudrate() && response.baudrates && response.baudrates.indexOf(response.baudratePreference) >= 0)
|
||||
self.selectedBaudrate(response.baudratePreference);
|
||||
}
|
||||
|
||||
self.fromStateResponse = function(response) {
|
||||
self.isErrorOrClosed(response.closedOrError);
|
||||
self.isOperational(response.operational);
|
||||
self.isPaused(response.paused);
|
||||
self.isPrinting(response.printing);
|
||||
self.isError(response.error);
|
||||
self.isReady(response.ready);
|
||||
self.isLoading(response.loading);
|
||||
}
|
||||
|
||||
self.connect = function() {
|
||||
if (self.isErrorOrClosed()) {
|
||||
$.ajax({
|
||||
url: AJAX_BASEURL + "control/connect",
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
data: { "port": self.selectedPort(), "baudrate": self.selectedBaudrate() }
|
||||
})
|
||||
} else {
|
||||
$.ajax({
|
||||
url: AJAX_BASEURL + "control/disconnect",
|
||||
type: "POST",
|
||||
dataType: "json"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
var connectionViewModel = new ConnectionViewModel();
|
||||
|
||||
function PrinterStateViewModel() {
|
||||
var self = this;
|
||||
|
||||
|
@ -8,7 +70,9 @@ function PrinterStateViewModel() {
|
|||
self.isPaused = ko.observable(undefined);
|
||||
self.isError = ko.observable(undefined);
|
||||
self.isReady = ko.observable(undefined);
|
||||
self.isLoading = ko.observable(undefined);
|
||||
|
||||
self.filename = ko.observable(undefined);
|
||||
self.filament = ko.observable(undefined);
|
||||
self.estimatedPrintTime = ko.observable(undefined);
|
||||
self.printTime = ko.observable(undefined);
|
||||
|
@ -35,14 +99,6 @@ function PrinterStateViewModel() {
|
|||
return "Pause";
|
||||
});
|
||||
|
||||
self.connect = function() {
|
||||
$.ajax({
|
||||
url: AJAX_BASEURL + "control/connect",
|
||||
type: 'POST',
|
||||
dataType: 'json'
|
||||
})
|
||||
}
|
||||
|
||||
self.fromResponse = function(response) {
|
||||
self.stateString(response.state);
|
||||
self.isErrorOrClosed(response.closedOrError);
|
||||
|
@ -51,8 +107,10 @@ function PrinterStateViewModel() {
|
|||
self.isPrinting(response.printing);
|
||||
self.isError(response.error);
|
||||
self.isReady(response.ready);
|
||||
self.isLoading(response.loading);
|
||||
|
||||
if (response.job) {
|
||||
self.filename(response.job.filename);
|
||||
self.filament(response.job.filament);
|
||||
self.estimatedPrintTime(response.job.estimatedPrintTime);
|
||||
self.printTime(response.job.printTime);
|
||||
|
@ -61,11 +119,17 @@ function PrinterStateViewModel() {
|
|||
self.totalLines(response.job.totalLines ? response.job.totalLines : 0);
|
||||
self.currentHeight(response.job.currentZ);
|
||||
} else {
|
||||
if (response.loading && response.gcode) {
|
||||
self.filename("Loading... (" + Math.round(response.gcode.progress * 100) + "%)");
|
||||
} else {
|
||||
self.filename(undefined);
|
||||
}
|
||||
self.filament(undefined);
|
||||
self.estimatedPrintTime(undefined);
|
||||
self.printTime(undefined);
|
||||
self.printTimeLeft(undefined);
|
||||
self.currentLine(undefined);
|
||||
self.totalLines(undefined);
|
||||
self.currentHeight(undefined);
|
||||
}
|
||||
}
|
||||
|
@ -79,12 +143,14 @@ function TemperatureViewModel() {
|
|||
self.bedTemp = ko.observable(undefined);
|
||||
self.targetTemp = ko.observable(undefined);
|
||||
self.bedTargetTemp = ko.observable(undefined);
|
||||
|
||||
self.isErrorOrClosed = ko.observable(undefined);
|
||||
self.isOperational = ko.observable(undefined);
|
||||
self.isPrinting = ko.observable(undefined);
|
||||
self.isPaused = ko.observable(undefined);
|
||||
self.isError = ko.observable(undefined);
|
||||
self.isReady = ko.observable(undefined);
|
||||
self.isLoading = ko.observable(undefined);
|
||||
|
||||
self.tempString = ko.computed(function() {
|
||||
if (!self.temp())
|
||||
|
@ -116,7 +182,6 @@ function TemperatureViewModel() {
|
|||
},
|
||||
xaxis: {
|
||||
mode: "time",
|
||||
timeformat: "%H:%M:%S",
|
||||
minTickSize: [2, "minute"],
|
||||
tickFormatter: function(val, axis) {
|
||||
var now = new Date();
|
||||
|
@ -146,6 +211,7 @@ function TemperatureViewModel() {
|
|||
self.isPrinting(response.printing);
|
||||
self.isError(response.error);
|
||||
self.isReady(response.ready);
|
||||
self.isLoading(response.loading);
|
||||
|
||||
self.updatePlot();
|
||||
}
|
||||
|
@ -174,9 +240,9 @@ function TerminalViewModel() {
|
|||
}
|
||||
|
||||
self.updateOutput = function() {
|
||||
var output = '';
|
||||
var output = "";
|
||||
for (var i = 0; i < self.log.length; i++) {
|
||||
output += self.log[i] + '\n';
|
||||
output += self.log[i] + "\n";
|
||||
}
|
||||
|
||||
var container = $("#terminal-output");
|
||||
|
@ -227,13 +293,14 @@ function GcodeFilesViewModel() {
|
|||
}
|
||||
var gcodeFilesViewModel = new GcodeFilesViewModel();
|
||||
|
||||
function DataUpdater(printerStateViewModel, temperatureViewModel, terminalViewModel) {
|
||||
function DataUpdater(connectionViewModel, printerStateViewModel, temperatureViewModel, terminalViewModel) {
|
||||
var self = this;
|
||||
|
||||
self.updateInterval = 500;
|
||||
self.includeTemperatures = true;
|
||||
self.includeLogs = true;
|
||||
|
||||
self.connectionViewModel = connectionViewModel;
|
||||
self.printerStateViewModel = printerStateViewModel;
|
||||
self.temperatureViewModel = temperatureViewModel;
|
||||
self.terminalViewModel = terminalViewModel;
|
||||
|
@ -248,48 +315,51 @@ function DataUpdater(printerStateViewModel, temperatureViewModel, terminalViewMo
|
|||
|
||||
$.ajax({
|
||||
url: AJAX_BASEURL + "state",
|
||||
type: 'GET',
|
||||
dataType: 'json',
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
data: parameters,
|
||||
success: function(response) {
|
||||
self.printerStateViewModel.fromResponse(response);
|
||||
self.connectionViewModel.fromStateResponse(response);
|
||||
|
||||
if (response.temperatures)
|
||||
self.temperatureViewModel.fromResponse(response);
|
||||
|
||||
if (response.log)
|
||||
self.terminalViewModel.fromResponse(response);
|
||||
},
|
||||
error: function(jqXHR, textState, errorThrows) {
|
||||
//alert(textState);
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(self.requestData, self.updateInterval);
|
||||
}
|
||||
}
|
||||
var dataUpdater = new DataUpdater(printerStateViewModel, temperatureViewModel, terminalViewModel);
|
||||
var dataUpdater = new DataUpdater(connectionViewModel, printerStateViewModel, temperatureViewModel, terminalViewModel);
|
||||
|
||||
$(function() {
|
||||
$("#printer_connect").click(printerStateViewModel.connect);
|
||||
$("#job_print").click(function() {
|
||||
$.ajax({
|
||||
url: AJAX_BASEURL + "control/print",
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
success: function(){}
|
||||
})
|
||||
})
|
||||
$("#job_pause").click(function() {
|
||||
$("#job_pause").button('toggle');
|
||||
$("#job_pause").button("toggle");
|
||||
$.ajax({
|
||||
url: AJAX_BASEURL + "control/pause",
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
type: "POST",
|
||||
dataType: "json"
|
||||
})
|
||||
})
|
||||
$("#job_cancel").click(function() {
|
||||
$.ajax({
|
||||
url: AJAX_BASEURL + "control/cancel",
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
type: "POST",
|
||||
dataType: "json"
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -343,26 +413,24 @@ $(function() {
|
|||
var command = $("#terminal-command").val();
|
||||
$.ajax({
|
||||
url: AJAX_BASEURL + "control/command",
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: 'command=' + command,
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
data: "command=" + command
|
||||
})
|
||||
})
|
||||
|
||||
$('#gcode_upload').fileupload({
|
||||
dataType: 'json',
|
||||
$("#gcode_upload").fileupload({
|
||||
dataType: "json",
|
||||
done: function (e, data) {
|
||||
gcodeFilesViewModel.fromResponse(data.result);
|
||||
},
|
||||
progressall: function (e, data) {
|
||||
var progress = parseInt(data.loaded / data.total * 100, 10);
|
||||
$('#gcode_upload_progress .bar').css(
|
||||
'width',
|
||||
progress + '%'
|
||||
);
|
||||
$("#gcode_upload_progress .bar").css("width", progress + "%");
|
||||
}
|
||||
});
|
||||
|
||||
ko.applyBindings(connectionViewModel, document.getElementById("connection"));
|
||||
ko.applyBindings(printerStateViewModel, document.getElementById("state"));
|
||||
ko.applyBindings(gcodeFilesViewModel, document.getElementById("files"));
|
||||
ko.applyBindings(temperatureViewModel, document.getElementById("temp"));
|
||||
|
@ -372,12 +440,20 @@ $(function() {
|
|||
dataUpdater.requestData();
|
||||
$.ajax({
|
||||
url: AJAX_BASEURL + "gcodefiles",
|
||||
method: 'GET',
|
||||
dataType: 'json',
|
||||
method: "GET",
|
||||
dataType: "json",
|
||||
success: function(response) {
|
||||
self.gcodeFilesViewModel.fromResponse(response);
|
||||
}
|
||||
});
|
||||
$.ajax({
|
||||
url: AJAX_BASEURL + "control/connectionOptions",
|
||||
method: "GET",
|
||||
dataType: "json",
|
||||
success: function(response) {
|
||||
connectionViewModel.fromResponse(response);
|
||||
}
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -32,15 +32,28 @@
|
|||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="accordion span4">
|
||||
<div class="accordion-group">
|
||||
<div class="accordion-heading">
|
||||
<a class="accordion-toggle" data-toggle="collapse" href="#connection"><i class="icon-signal"></i> Connection</a>
|
||||
</div>
|
||||
<div class="accordion-body collapse in" id="connection">
|
||||
<div class="accordion-inner">
|
||||
<label for="connection_ports" data-bind="css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed">Serial Port</label>
|
||||
<select id="connection_ports" data-bind="options: portOptions, optionsCaption: 'AUTO', value: selectedPort, css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed"></select>
|
||||
<label for="connection_baudrates" data-bind="css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed">Baudrate</label>
|
||||
<select id="connection_baudrates" data-bind="options: baudrateOptions, optionsCaption: 'AUTO', value: selectedBaudrate, css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed"></select>
|
||||
<button class="btn btn-block" id="printer_connect" data-bind="click: connect, text: buttonText()">Connect</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-group">
|
||||
<div class="accordion-heading">
|
||||
<a class="accordion-toggle" data-toggle="collapse" href="#state"><i class="icon-info-sign"></i> State</a>
|
||||
</div>
|
||||
<div class="accordion-body collapse in" id="state">
|
||||
<div class="accordion-inner">
|
||||
<button class="btn btn-block" id="printer_connect" data-bind="css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed">Connect</button>
|
||||
|
||||
Machine State: <strong data-bind="text: stateString"></strong><br>
|
||||
File: <strong data-bind="text: filename"></strong><br>
|
||||
Filament: <strong data-bind="text: filament"></strong><br>
|
||||
Estimated Print Time: <strong data-bind="text: estimatedPrintTime"></strong><br>
|
||||
Line: <strong data-bind="text: lineString"></strong><br>
|
||||
|
@ -144,21 +157,21 @@
|
|||
<div class="tab-pane" id="jog">
|
||||
<div style="width: 350px; height: 70px">
|
||||
<div style="width: 70px; float: left;"> </div>
|
||||
<div style="width: 70px; float: left;"><button class="btn btn-block" id="jog_y_inc" data-bind="enable: isOperational() && isReady() && !isPrinting()">Up</button></div>
|
||||
<div style="width: 70px; float: left;"><button class="btn btn-block" id="jog_y_inc" data-bind="enable: isOperational() && !isPrinting()">Up</button></div>
|
||||
<div style="width: 70px; float: left;"> </div>
|
||||
<div style="width: 70px; float: left; margin-left: 20px"><button class="btn btn-block" id="jog_z_inc" data-bind="enable: isOperational() && isReady() && !isPrinting()">Z+</button></div>
|
||||
<div style="width: 70px; float: left; margin-left: 20px"><button class="btn btn-block" id="jog_z_inc" data-bind="enable: isOperational() && !isPrinting()">Z+</button></div>
|
||||
</div>
|
||||
<div style="width: 350px; height: 70px">
|
||||
<div style="width: 70px; float: left;"><button class="btn btn-block" id="jog_x_dec" data-bind="enable: isOperational() && isReady() && !isPrinting()">Left</button></div>
|
||||
<div style="width: 70px; float: left;"><button class="btn btn-block" id="jog_xy_home" data-bind="enable: isOperational() && isReady() && !isPrinting()">Home</button></div>
|
||||
<div style="width: 70px; float: left;"><button class="btn btn-block" id="jog_x_inc" data-bind="enable: isOperational() && isReady() && !isPrinting()">Right</button></div>
|
||||
<div style="width: 70px; float: left; margin-left: 20px"><button class="btn btn-block" id="jog_z_home" data-bind="enable: isOperational() && isReady() && !isPrinting()">Home</button></div>
|
||||
<div style="width: 70px; float: left;"><button class="btn btn-block" id="jog_x_dec" data-bind="enable: isOperational() && !isPrinting()">Left</button></div>
|
||||
<div style="width: 70px; float: left;"><button class="btn btn-block" id="jog_xy_home" data-bind="enable: isOperational() && !isPrinting()">Home</button></div>
|
||||
<div style="width: 70px; float: left;"><button class="btn btn-block" id="jog_x_inc" data-bind="enable: isOperational() && !isPrinting()">Right</button></div>
|
||||
<div style="width: 70px; float: left; margin-left: 20px"><button class="btn btn-block" id="jog_z_home" data-bind="enable: isOperational() && !isPrinting()">Home</button></div>
|
||||
</div>
|
||||
<div style="width: 350px; height: 70px">
|
||||
<div style="width: 70px; float: left;"> </div>
|
||||
<div style="width: 70px; float: left;"><button class="btn btn-block" id="jog_y_dec" data-bind="enable: isOperational() && isReady() && !isPrinting()">Down</button></div>
|
||||
<div style="width: 70px; float: left;"><button class="btn btn-block" id="jog_y_dec" data-bind="enable: isOperational() && !isPrinting()">Down</button></div>
|
||||
<div style="width: 70px; float: left;"> </div>
|
||||
<div style="width: 70px; float: left; margin-left: 20px"><button class="btn btn-block" id="jog_z_dec" data-bind="enable: isOperational() && isReady() && !isPrinting()">Z-</button></div>
|
||||
<div style="width: 70px; float: left; margin-left: 20px"><button class="btn btn-block" id="jog_z_dec" data-bind="enable: isOperational() && !isPrinting()">Z-</button></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane" id="speed">
|
||||
|
|
Loading…
Reference in New Issue