- 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 documentation
master
Gina Häußge 2012-12-28 20:37:40 +01:00
parent 083498865d
commit f40460d797
5 changed files with 404 additions and 159 deletions

View File

@ -1,11 +1,11 @@
#!/usr/bin/env python #!/usr/bin/env python
# coding=utf-8 # 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 flask import Flask, request, render_template, jsonify, make_response
from werkzeug import secure_filename from werkzeug import secure_filename
from printer import Printer from printer import Printer, getConnectionOptions
import sys import sys
import os import os
@ -16,15 +16,15 @@ BASEURL="/ajax/"
SUCCESS={} SUCCESS={}
# taken from http://stackoverflow.com/questions/1084697/how-do-i-store-desktop-application-data-in-a-cross-platform-way-for-python # 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 from AppKit import NSSearchPathForDirectoriesInDomains
# http://developer.apple.com/DOCUMENTATION/Cocoa/Reference/Foundation/Miscellaneous/Foundation_Functions/Reference/reference.html#//apple_ref/c/func/NSSearchPathForDirectoriesInDomains # http://developer.apple.com/DOCUMENTATION/Cocoa/Reference/Foundation/Miscellaneous/Foundation_Functions/Reference/reference.html#//apple_ref/c/func/NSSearchPathForDirectoriesInDomains
# NSApplicationSupportDirectory = 14 # NSApplicationSupportDirectory = 14
# NSUserDomainMask = 1 # NSUserDomainMask = 1
# True for expanding the tilde into a fully qualified path # True for expanding the tilde into a fully qualified path
appdata = os.path.join(NSSearchPathForDirectoriesInDomains(14, 1, True)[0], APPNAME) appdata = os.path.join(NSSearchPathForDirectoriesInDomains(14, 1, True)[0], APPNAME)
elif sys.platform == 'win32': elif sys.platform == "win32":
appdata = os.path.join(os.environ['APPDATA'], APPNAME) appdata = os.path.join(os.environ["APPDATA"], APPNAME)
else: else:
appdata = os.path.expanduser(os.path.join("~", "." + APPNAME.lower())) appdata = os.path.expanduser(os.path.join("~", "." + APPNAME.lower()))
@ -36,94 +36,111 @@ ALLOWED_EXTENSIONS = set(["gcode"])
app = Flask("Cura.webui") app = Flask("Cura.webui")
printer = Printer() printer = Printer()
@app.route('/') @app.route("/")
def index(): def index():
return render_template('index.html') return render_template("index.html")
#~~ Printer state #~~ Printer state
@app.route(BASEURL + 'state', methods=['GET']) @app.route(BASEURL + "state", methods=["GET"])
def printerState(): def printerState():
temp = printer.currentTemp temp = printer.currentTemp
bedTemp = printer.currentBedTemp bedTemp = printer.currentBedTemp
targetTemp = printer.currentTargetTemp targetTemp = printer.currentTargetTemp
bedTargetTemp = printer.currentBedTargetTemp bedTargetTemp = printer.currentBedTargetTemp
jobData = printer.jobData() jobData = printer.jobData()
gcodeState = printer.gcodeState()
result = { result = {
'state': printer.getStateString(), "state": printer.getStateString(),
'temp': temp, "temp": temp,
'bedTemp': bedTemp, "bedTemp": bedTemp,
'targetTemp': targetTemp, "targetTemp": targetTemp,
'targetBedTemp': bedTargetTemp, "targetBedTemp": bedTargetTemp,
'operational': printer.isOperational(), "operational": printer.isOperational(),
'closedOrError': printer.isClosedOrError(), "closedOrError": printer.isClosedOrError(),
'error': printer.isError(), "error": printer.isError(),
'printing': printer.isPrinting(), "printing": printer.isPrinting(),
'paused': printer.isPaused(), "paused": printer.isPaused(),
'ready': printer.isReady() "ready": printer.isReady(),
"loading": printer.isLoading()
} }
if (jobData != None): if jobData is not None:
result['job'] = jobData jobData["filename"] = jobData["filename"].replace(UPLOAD_FOLDER + os.sep, "")
result["job"] = jobData
if (request.values.has_key('temperatures')): if gcodeState is not None:
result['temperatures'] = printer.temps gcodeState["filename"] = gcodeState["filename"].replace(UPLOAD_FOLDER + os.sep, "")
result["gcode"] = gcodeState
if (request.values.has_key('log')): if request.values.has_key("temperatures"):
result['log'] = printer.log result["temperatures"] = printer.temps
if (request.values.has_key('messages')): if request.values.has_key("log"):
result['messages'] = printer.messages result["log"] = printer.log
if request.values.has_key("messages"):
result["messages"] = printer.messages
return jsonify(result) return jsonify(result)
@app.route(BASEURL + 'state/messages', methods=['GET']) @app.route(BASEURL + "state/messages", methods=["GET"])
def printerMessages(): def printerMessages():
return jsonify(messages=printer.messages) return jsonify(messages=printer.messages)
@app.route(BASEURL + 'state/log', methods=['GET']) @app.route(BASEURL + "state/log", methods=["GET"])
def printerLogs(): def printerLogs():
return jsonify(log=printer.log) return jsonify(log=printer.log)
@app.route(BASEURL + 'state/temperatures', methods=['GET']) @app.route(BASEURL + "state/temperatures", methods=["GET"])
def printerTemperatures(): def printerTemperatures():
return jsonify(temperatures = printer.temps) return jsonify(temperatures = printer.temps)
#~~ Printer control #~~ Printer control
@app.route(BASEURL + 'control/connect', methods=['POST']) @app.route(BASEURL + "control/connectionOptions", methods=["GET"])
def connect(): def connectionOptions():
printer.connect() return jsonify(getConnectionOptions())
return jsonify(state='Connecting')
@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(): def disconnect():
printer.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(): def printerCommand():
command = request.form['command'] command = request.form["command"]
printer.command(command) printer.command(command)
return jsonify(SUCCESS) return jsonify(SUCCESS)
@app.route(BASEURL + 'control/print', methods=['POST']) @app.route(BASEURL + "control/print", methods=["POST"])
def printGcode(): def printGcode():
printer.startPrint() printer.startPrint()
return jsonify(SUCCESS) return jsonify(SUCCESS)
@app.route(BASEURL + 'control/pause', methods=['POST']) @app.route(BASEURL + "control/pause", methods=["POST"])
def pausePrint(): def pausePrint():
printer.togglePausePrint() printer.togglePausePrint()
return jsonify(SUCCESS) return jsonify(SUCCESS)
@app.route(BASEURL + 'control/cancel', methods=['POST']) @app.route(BASEURL + "control/cancel", methods=["POST"])
def cancelPrint(): def cancelPrint():
printer.cancelPrint() printer.cancelPrint()
return jsonify(SUCCESS) return jsonify(SUCCESS)
@app.route(BASEURL + 'control/temperature', methods=['POST']) @app.route(BASEURL + "control/temperature", methods=["POST"])
def setTargetTemperature(): def setTargetTemperature():
if not printer.isOperational(): if not printer.isOperational():
return jsonify(SUCCESS) return jsonify(SUCCESS)
@ -143,7 +160,7 @@ def setTargetTemperature():
@app.route(BASEURL + "control/jog", methods=["POST"]) @app.route(BASEURL + "control/jog", methods=["POST"])
def jog(): def jog():
if not printer.isOperational() or printer.isPrinting(): 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) return jsonify(SUCCESS)
if request.values.has_key("x"): if request.values.has_key("x"):
@ -169,7 +186,7 @@ def jog():
#~~ GCODE file handling #~~ GCODE file handling
@app.route(BASEURL + 'gcodefiles', methods=['GET']) @app.route(BASEURL + "gcodefiles", methods=["GET"])
def readGcodeFiles(): def readGcodeFiles():
files = [] files = []
for osFile in os.listdir(UPLOAD_FOLDER): for osFile in os.listdir(UPLOAD_FOLDER):
@ -181,22 +198,22 @@ def readGcodeFiles():
}) })
return jsonify(files=files) return jsonify(files=files)
@app.route(BASEURL + 'gcodefiles/upload', methods=['POST']) @app.route(BASEURL + "gcodefiles/upload", methods=["POST"])
def uploadGcodeFile(): def uploadGcodeFile():
file = request.files['gcode_file'] file = request.files["gcode_file"]
if file and allowed_file(file.filename): if file and allowed_file(file.filename):
secure = secure_filename(file.filename) secure = secure_filename(file.filename)
filename = os.path.join(UPLOAD_FOLDER, secure) filename = os.path.join(UPLOAD_FOLDER, secure)
file.save(filename) file.save(filename)
return readGcodeFiles() return readGcodeFiles()
@app.route(BASEURL + 'gcodefiles/load', methods=['POST']) @app.route(BASEURL + "gcodefiles/load", methods=["POST"])
def loadGcodeFile(): def loadGcodeFile():
filename = request.values["filename"] filename = request.values["filename"]
printer.loadGcode(UPLOAD_FOLDER + os.sep + filename) printer.loadGcode(UPLOAD_FOLDER + os.sep + filename)
return jsonify(SUCCESS) return jsonify(SUCCESS)
@app.route(BASEURL + 'gcodefiles/delete', methods=['POST']) @app.route(BASEURL + "gcodefiles/delete", methods=["POST"])
def deleteGcodeFile(): def deleteGcodeFile():
if request.values.has_key("filename"): if request.values.has_key("filename"):
filename = request.values["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 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: if num < 1024.0:
return "%3.1f%s" % (num, x) return "%3.1f%s" % (num, x)
num /= 1024.0 num /= 1024.0
return "%3.1f%s" % (num, 'TB') return "%3.1f%s" % (num, "TB")
def allowed_file(filename): def allowed_file(filename):
return "." in filename and filename.rsplit(".", 1)[1] in ALLOWED_EXTENSIONS return "." in filename and filename.rsplit(".", 1)[1] in ALLOWED_EXTENSIONS
def run(): def run():
app.debug = True
app.run(host="0.0.0.0", port=5000) app.run(host="0.0.0.0", port=5000)

View File

@ -1,20 +1,33 @@
# coding=utf-8 # coding=utf-8
__author__ = 'Gina Häußge <osd@foosel.net>' __author__ = "Gina Häußge <osd@foosel.net>"
import time import time
import os import os
from threading import Thread
import Cura.util.machineCom as machineCom import Cura.util.machineCom as machineCom
from Cura.util import gcodeInterpreter 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(): class Printer():
def __init__(self): def __init__(self):
# state # state
self.temps = { self.temps = {
'actual': [], "actual": [],
'target': [], "target": [],
'actualBed': [], "actualBed": [],
'targetBed': [] "targetBed": []
} }
self.messages = [] self.messages = []
self.log = [] self.log = []
@ -32,44 +45,68 @@ class Printer():
self.gcodeList = None self.gcodeList = None
self.filename = None self.filename = None
self.gcodeLoader = None
# comm # comm
self.comm = None self.comm = None
def connect(self): def connect(self, port=None, baudrate=None):
if self.comm != 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.close()
self.comm = machineCom.MachineCom(callbackObject=self) self.comm = machineCom.MachineCom(port, baudrate, callbackObject=self)
def disconnect(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.close()
self.comm = None self.comm = None
def command(self, command): def command(self, command):
"""
Sends a single gcode command to the printer.
"""
self.commands([command]) self.commands([command])
def commands(self, commands): def commands(self, commands):
"""
Sends multiple gcode commands (provided as a list) to the printer.
"""
for command in commands: for command in commands:
self.comm.sendCommand(command) self.comm.sendCommand(command)
def mcLog(self, message): 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.append(message)
self.log = self.log[-300:] self.log = self.log[-300:]
def mcTempUpdate(self, temp, bedTemp, targetTemp, bedTargetTemp): 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) currentTime = int(time.time() * 1000)
self.temps['actual'].append((currentTime, temp)) self.temps["actual"].append((currentTime, temp))
self.temps['actual'] = self.temps['actual'][-300:] self.temps["actual"] = self.temps["actual"][-300:]
self.temps['target'].append((currentTime, targetTemp)) self.temps["target"].append((currentTime, targetTemp))
self.temps['target'] = self.temps['target'][-300:] self.temps["target"] = self.temps["target"][-300:]
self.temps['actualBed'].append((currentTime, bedTemp)) self.temps["actualBed"].append((currentTime, bedTemp))
self.temps['actualBed'] = self.temps['actualBed'][-300:] self.temps["actualBed"] = self.temps["actualBed"][-300:]
self.temps['targetBed'].append((currentTime, bedTargetTemp)) self.temps["targetBed"].append((currentTime, bedTargetTemp))
self.temps['targetBed'] = self.temps['targetBed'][-300:] self.temps["targetBed"] = self.temps["targetBed"][-300:]
self.currentTemp = temp self.currentTemp = temp
self.currentTargetTemp = targetTemp self.currentTargetTemp = targetTemp
@ -77,22 +114,55 @@ class Printer():
self.currentBedTargetTemp = bedTargetTemp self.currentBedTargetTemp = bedTargetTemp
def mcStateChange(self, state): 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 self.state = state
def mcMessage(self, message): 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.append(message)
self.messages = self.messages[-300:] self.messages = self.messages[-300:]
def mcProgress(self, lineNr): 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.printTime = self.comm.getPrintTime()
self.printTimeLeft = self.comm.getPrintTimeRemainingEstimate() self.printTimeLeft = self.comm.getPrintTimeRemainingEstimate()
self.progress = self.comm.getPrintPos() self.progress = self.comm.getPrintPos()
def mcZChange(self, newZ): def mcZChange(self, newZ):
"""
Callback method for the comm object, called upon change of the z-layer.
"""
self.currentZ = newZ 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): 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 formattedPrintTime = None
if (self.printTime): if (self.printTime):
formattedPrintTime = "%02d:%02d" % (int(self.printTime / 60), int(self.printTime % 60)) 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)) formattedPrintTimeLeft = "%02d:%02d" % (int(self.printTimeLeft / 60), int(self.printTimeLeft % 60))
data = { data = {
'currentZ': self.currentZ, "filename": self.filename,
'line': self.progress, "currentZ": self.currentZ,
'totalLines': len(self.gcodeList), "line": self.progress,
'printTime': formattedPrintTime, "totalLines": len(self.gcodeList),
'printTimeLeft': formattedPrintTimeLeft, "printTime": formattedPrintTime,
'filament': "%.2fm %.2fg" % ( "printTimeLeft": formattedPrintTimeLeft,
"filament": "%.2fm %.2fg" % (
self.gcode.extrusionAmount / 1000, self.gcode.extrusionAmount / 1000,
self.gcode.calculateWeight() * 1000 self.gcode.calculateWeight() * 1000
), ),
'estimatedPrintTime': "%02d:%02d" % ( "estimatedPrintTime": "%02d:%02d" % (
int(self.gcode.totalMoveTimeMinute / 60), int(self.gcode.totalMoveTimeMinute / 60),
int(self.gcode.totalMoveTimeMinute % 60) int(self.gcode.totalMoveTimeMinute % 60)
) )
@ -120,64 +191,68 @@ class Printer():
data = None data = None
return data 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): 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: else:
return self.comm.getStateString() return self.comm.getStateString()
def isClosedOrError(self): def isClosedOrError(self):
return self.comm == None or self.comm.isClosedOrError() return self.comm is None or self.comm.isClosedOrError()
def isOperational(self): def isOperational(self):
return self.comm != None and self.comm.isOperational() return self.comm is not None and self.comm.isOperational()
def isPrinting(self): def isPrinting(self):
return self.comm != None and self.comm.isPrinting() return self.comm is not None and self.comm.isPrinting()
def isPaused(self): def isPaused(self):
return self.comm != None and self.comm.isPaused() return self.comm is not None and self.comm.isPaused()
def isError(self): def isError(self):
return self.comm != None and self.comm.isError() return self.comm is not None and self.comm.isError()
def isReady(self): 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): 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 return
#Send an initial M110 to reset the line counter to zero. self.filename = None
prevLineType = lineType = 'CUSTOM' self.gcode = None
gcodeList = ["M110"] self.gcodeList = None
for line in open(file, 'r'):
if line.startswith(';TYPE:'): self.gcodeLoader = GcodeLoader(file, self)
lineType = line[6:].strip() self.gcodeLoader.start()
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
def startPrint(self): 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 return
if self.gcodeList == None: if self.gcodeList is None:
return return
if self.comm.isPrinting(): if self.comm.isPrinting():
return return
@ -185,13 +260,72 @@ class Printer():
self.comm.printGCode(self.gcodeList) self.comm.printGCode(self.gcodeList)
def togglePausePrint(self): def togglePausePrint(self):
if self.comm == None: """
Pause the current printjob.
"""
if self.comm is None:
return return
self.comm.setPause(not self.comm.isPaused()) self.comm.setPause(not self.comm.isPaused())
def cancelPrint(self): def cancelPrint(self, disableMotorsAndHeater=True):
if self.comm == None: """
Cancel the current printjob.
"""
if self.comm is None:
return return
self.comm.cancelPrint() 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

View File

@ -50,4 +50,8 @@ table th.gcode_files_action, table td.gcode_files_action {
#temp_newTemp { #temp_newTemp {
text-align: right; text-align: right;
}
#connection_ports, #connection_baudrates {
width: 100%;
} }

View File

@ -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() { function PrinterStateViewModel() {
var self = this; var self = this;
@ -8,7 +70,9 @@ function PrinterStateViewModel() {
self.isPaused = ko.observable(undefined); self.isPaused = ko.observable(undefined);
self.isError = ko.observable(undefined); self.isError = ko.observable(undefined);
self.isReady = ko.observable(undefined); self.isReady = ko.observable(undefined);
self.isLoading = ko.observable(undefined);
self.filename = ko.observable(undefined);
self.filament = ko.observable(undefined); self.filament = ko.observable(undefined);
self.estimatedPrintTime = ko.observable(undefined); self.estimatedPrintTime = ko.observable(undefined);
self.printTime = ko.observable(undefined); self.printTime = ko.observable(undefined);
@ -35,14 +99,6 @@ function PrinterStateViewModel() {
return "Pause"; return "Pause";
}); });
self.connect = function() {
$.ajax({
url: AJAX_BASEURL + "control/connect",
type: 'POST',
dataType: 'json'
})
}
self.fromResponse = function(response) { self.fromResponse = function(response) {
self.stateString(response.state); self.stateString(response.state);
self.isErrorOrClosed(response.closedOrError); self.isErrorOrClosed(response.closedOrError);
@ -51,8 +107,10 @@ function PrinterStateViewModel() {
self.isPrinting(response.printing); self.isPrinting(response.printing);
self.isError(response.error); self.isError(response.error);
self.isReady(response.ready); self.isReady(response.ready);
self.isLoading(response.loading);
if (response.job) { if (response.job) {
self.filename(response.job.filename);
self.filament(response.job.filament); self.filament(response.job.filament);
self.estimatedPrintTime(response.job.estimatedPrintTime); self.estimatedPrintTime(response.job.estimatedPrintTime);
self.printTime(response.job.printTime); self.printTime(response.job.printTime);
@ -61,11 +119,17 @@ function PrinterStateViewModel() {
self.totalLines(response.job.totalLines ? response.job.totalLines : 0); self.totalLines(response.job.totalLines ? response.job.totalLines : 0);
self.currentHeight(response.job.currentZ); self.currentHeight(response.job.currentZ);
} else { } else {
if (response.loading && response.gcode) {
self.filename("Loading... (" + Math.round(response.gcode.progress * 100) + "%)");
} else {
self.filename(undefined);
}
self.filament(undefined); self.filament(undefined);
self.estimatedPrintTime(undefined); self.estimatedPrintTime(undefined);
self.printTime(undefined); self.printTime(undefined);
self.printTimeLeft(undefined); self.printTimeLeft(undefined);
self.currentLine(undefined); self.currentLine(undefined);
self.totalLines(undefined);
self.currentHeight(undefined); self.currentHeight(undefined);
} }
} }
@ -79,12 +143,14 @@ function TemperatureViewModel() {
self.bedTemp = ko.observable(undefined); self.bedTemp = ko.observable(undefined);
self.targetTemp = ko.observable(undefined); self.targetTemp = ko.observable(undefined);
self.bedTargetTemp = ko.observable(undefined); self.bedTargetTemp = ko.observable(undefined);
self.isErrorOrClosed = ko.observable(undefined); self.isErrorOrClosed = ko.observable(undefined);
self.isOperational = ko.observable(undefined); self.isOperational = ko.observable(undefined);
self.isPrinting = ko.observable(undefined); self.isPrinting = ko.observable(undefined);
self.isPaused = ko.observable(undefined); self.isPaused = ko.observable(undefined);
self.isError = ko.observable(undefined); self.isError = ko.observable(undefined);
self.isReady = ko.observable(undefined); self.isReady = ko.observable(undefined);
self.isLoading = ko.observable(undefined);
self.tempString = ko.computed(function() { self.tempString = ko.computed(function() {
if (!self.temp()) if (!self.temp())
@ -116,7 +182,6 @@ function TemperatureViewModel() {
}, },
xaxis: { xaxis: {
mode: "time", mode: "time",
timeformat: "%H:%M:%S",
minTickSize: [2, "minute"], minTickSize: [2, "minute"],
tickFormatter: function(val, axis) { tickFormatter: function(val, axis) {
var now = new Date(); var now = new Date();
@ -146,6 +211,7 @@ function TemperatureViewModel() {
self.isPrinting(response.printing); self.isPrinting(response.printing);
self.isError(response.error); self.isError(response.error);
self.isReady(response.ready); self.isReady(response.ready);
self.isLoading(response.loading);
self.updatePlot(); self.updatePlot();
} }
@ -174,9 +240,9 @@ function TerminalViewModel() {
} }
self.updateOutput = function() { self.updateOutput = function() {
var output = ''; var output = "";
for (var i = 0; i < self.log.length; i++) { for (var i = 0; i < self.log.length; i++) {
output += self.log[i] + '\n'; output += self.log[i] + "\n";
} }
var container = $("#terminal-output"); var container = $("#terminal-output");
@ -227,13 +293,14 @@ function GcodeFilesViewModel() {
} }
var gcodeFilesViewModel = new GcodeFilesViewModel(); var gcodeFilesViewModel = new GcodeFilesViewModel();
function DataUpdater(printerStateViewModel, temperatureViewModel, terminalViewModel) { function DataUpdater(connectionViewModel, printerStateViewModel, temperatureViewModel, terminalViewModel) {
var self = this; var self = this;
self.updateInterval = 500; self.updateInterval = 500;
self.includeTemperatures = true; self.includeTemperatures = true;
self.includeLogs = true; self.includeLogs = true;
self.connectionViewModel = connectionViewModel;
self.printerStateViewModel = printerStateViewModel; self.printerStateViewModel = printerStateViewModel;
self.temperatureViewModel = temperatureViewModel; self.temperatureViewModel = temperatureViewModel;
self.terminalViewModel = terminalViewModel; self.terminalViewModel = terminalViewModel;
@ -248,48 +315,51 @@ function DataUpdater(printerStateViewModel, temperatureViewModel, terminalViewMo
$.ajax({ $.ajax({
url: AJAX_BASEURL + "state", url: AJAX_BASEURL + "state",
type: 'GET', type: "GET",
dataType: 'json', dataType: "json",
data: parameters, data: parameters,
success: function(response) { success: function(response) {
self.printerStateViewModel.fromResponse(response); self.printerStateViewModel.fromResponse(response);
self.connectionViewModel.fromStateResponse(response);
if (response.temperatures) if (response.temperatures)
self.temperatureViewModel.fromResponse(response); self.temperatureViewModel.fromResponse(response);
if (response.log) if (response.log)
self.terminalViewModel.fromResponse(response); self.terminalViewModel.fromResponse(response);
},
error: function(jqXHR, textState, errorThrows) {
//alert(textState);
} }
}); });
setTimeout(self.requestData, self.updateInterval); setTimeout(self.requestData, self.updateInterval);
} }
} }
var dataUpdater = new DataUpdater(printerStateViewModel, temperatureViewModel, terminalViewModel); var dataUpdater = new DataUpdater(connectionViewModel, printerStateViewModel, temperatureViewModel, terminalViewModel);
$(function() { $(function() {
$("#printer_connect").click(printerStateViewModel.connect);
$("#job_print").click(function() { $("#job_print").click(function() {
$.ajax({ $.ajax({
url: AJAX_BASEURL + "control/print", url: AJAX_BASEURL + "control/print",
type: 'POST', type: "POST",
dataType: 'json', dataType: "json",
success: function(){} success: function(){}
}) })
}) })
$("#job_pause").click(function() { $("#job_pause").click(function() {
$("#job_pause").button('toggle'); $("#job_pause").button("toggle");
$.ajax({ $.ajax({
url: AJAX_BASEURL + "control/pause", url: AJAX_BASEURL + "control/pause",
type: 'POST', type: "POST",
dataType: 'json', dataType: "json"
}) })
}) })
$("#job_cancel").click(function() { $("#job_cancel").click(function() {
$.ajax({ $.ajax({
url: AJAX_BASEURL + "control/cancel", url: AJAX_BASEURL + "control/cancel",
type: 'POST', type: "POST",
dataType: 'json', dataType: "json"
}) })
}) })
@ -343,26 +413,24 @@ $(function() {
var command = $("#terminal-command").val(); var command = $("#terminal-command").val();
$.ajax({ $.ajax({
url: AJAX_BASEURL + "control/command", url: AJAX_BASEURL + "control/command",
type: 'POST', type: "POST",
dataType: 'json', dataType: "json",
data: 'command=' + command, data: "command=" + command
}) })
}) })
$('#gcode_upload').fileupload({ $("#gcode_upload").fileupload({
dataType: 'json', dataType: "json",
done: function (e, data) { done: function (e, data) {
gcodeFilesViewModel.fromResponse(data.result); gcodeFilesViewModel.fromResponse(data.result);
}, },
progressall: function (e, data) { progressall: function (e, data) {
var progress = parseInt(data.loaded / data.total * 100, 10); var progress = parseInt(data.loaded / data.total * 100, 10);
$('#gcode_upload_progress .bar').css( $("#gcode_upload_progress .bar").css("width", progress + "%");
'width',
progress + '%'
);
} }
}); });
ko.applyBindings(connectionViewModel, document.getElementById("connection"));
ko.applyBindings(printerStateViewModel, document.getElementById("state")); ko.applyBindings(printerStateViewModel, document.getElementById("state"));
ko.applyBindings(gcodeFilesViewModel, document.getElementById("files")); ko.applyBindings(gcodeFilesViewModel, document.getElementById("files"));
ko.applyBindings(temperatureViewModel, document.getElementById("temp")); ko.applyBindings(temperatureViewModel, document.getElementById("temp"));
@ -372,12 +440,20 @@ $(function() {
dataUpdater.requestData(); dataUpdater.requestData();
$.ajax({ $.ajax({
url: AJAX_BASEURL + "gcodefiles", url: AJAX_BASEURL + "gcodefiles",
method: 'GET', method: "GET",
dataType: 'json', dataType: "json",
success: function(response) { success: function(response) {
self.gcodeFilesViewModel.fromResponse(response); self.gcodeFilesViewModel.fromResponse(response);
} }
}); });
$.ajax({
url: AJAX_BASEURL + "control/connectionOptions",
method: "GET",
dataType: "json",
success: function(response) {
connectionViewModel.fromResponse(response);
}
})
} }
); );

View File

@ -32,15 +32,28 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="accordion span4"> <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-group">
<div class="accordion-heading"> <div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#state"><i class="icon-info-sign"></i> State</a> <a class="accordion-toggle" data-toggle="collapse" href="#state"><i class="icon-info-sign"></i> State</a>
</div> </div>
<div class="accordion-body collapse in" id="state"> <div class="accordion-body collapse in" id="state">
<div class="accordion-inner"> <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> 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> Filament: <strong data-bind="text: filament"></strong><br>
Estimated Print Time: <strong data-bind="text: estimatedPrintTime"></strong><br> Estimated Print Time: <strong data-bind="text: estimatedPrintTime"></strong><br>
Line: <strong data-bind="text: lineString"></strong><br> Line: <strong data-bind="text: lineString"></strong><br>
@ -144,21 +157,21 @@
<div class="tab-pane" id="jog"> <div class="tab-pane" id="jog">
<div style="width: 350px; height: 70px"> <div style="width: 350px; height: 70px">
<div style="width: 70px; float: left;">&nbsp;</div> <div style="width: 70px; float: left;">&nbsp;</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;">&nbsp;</div> <div style="width: 70px; float: left;">&nbsp;</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>
<div style="width: 350px; height: 70px"> <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_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() && isReady() && !isPrinting()">Home</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() && isReady() && !isPrinting()">Right</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() && isReady() && !isPrinting()">Home</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>
<div style="width: 350px; height: 70px"> <div style="width: 350px; height: 70px">
<div style="width: 70px; float: left;">&nbsp;</div> <div style="width: 70px; float: left;">&nbsp;</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;">&nbsp;</div> <div style="width: 70px; float: left;">&nbsp;</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> </div>
<div class="tab-pane" id="speed"> <div class="tab-pane" id="speed">