Refactoring server/client communication to utilize socket.io instead of 500ms-polling for server-side updates

master
Gina Häußge 2013-01-06 16:51:04 +01:00
parent a777c369c7
commit ddc3ff9563
9 changed files with 4410 additions and 345 deletions

View File

@ -22,62 +22,80 @@ def getConnectionOptions():
"baudratePreference": settings().getInt("serial", "baudrate") "baudratePreference": settings().getInt("serial", "baudrate")
} }
def _getFormattedTimeDelta(d):
hours = d.seconds // 3600
minutes = (d.seconds % 3600) // 60
seconds = d.seconds % 60
return "%02d:%02d:%02d" % (hours, minutes, seconds)
class Printer(): class Printer():
def __init__(self): def __init__(self):
# state # state
self.temps = { self._temp = None
self._bedTemp = None
self._targetTemp = None
self._targetBedTemp = None
self._temps = {
"actual": [], "actual": [],
"target": [], "target": [],
"actualBed": [], "actualBed": [],
"targetBed": [] "targetBed": []
} }
self.messages = []
self.log = []
self.state = None
self.currentZ = None
self.progress = None
self.printTime = None
self.printTimeLeft = None
self.currentTemp = None
self.currentBedTemp = None
self.currentTargetTemp = None
self.currentBedTargetTemp = None
self.gcode = None self._latestMessage = None
self.gcodeList = None self._messages = []
self.filename = None
self.gcodeLoader = None self._latestLog = None
self._log = []
self.feedrateModifierMapping = {"outerWall": "WALL-OUTER", "innerWall": "WALL_INNER", "fill": "FILL", "support": "SUPPORT"} self._state = None
self.timelapse = None self._currentZ = None
self._progress = None
self._printTime = None
self._printTimeLeft = None
# gcode handling
self._gcode = None
self._gcodeList = None
self._filename = None
self._gcodeLoader = None
# feedrate
self._feedrateModifierMapping = {"outerWall": "WALL-OUTER", "innerWall": "WALL_INNER", "fill": "FILL", "support": "SUPPORT"}
# timelapse
self._timelapse = None
# comm # comm
self.comm = None self._comm = None
# callbacks
self._callbacks = []
#~~ callback registration
def registerCallback(self, callback):
self._callbacks.append(callback)
self._sendInitialStateUpdate(callback)
def unregisterCallback(self, callback):
if callback in self._callbacks:
self._callbacks.remove(callback)
#~~ printer commands
def connect(self, port=None, baudrate=None): def connect(self, port=None, baudrate=None):
""" """
Connects to the printer. If port and/or baudrate is provided, uses these settings, otherwise autodetection Connects to the printer. If port and/or baudrate is provided, uses these settings, otherwise autodetection
will be attempted. will be attempted.
""" """
if self.comm is not None: if self._comm is not None:
self.comm.close() self._comm.close()
self.comm = comm.MachineCom(port, baudrate, callbackObject=self) self._comm = comm.MachineCom(port, baudrate, callbackObject=self)
def disconnect(self): def disconnect(self):
""" """
Closes the connection to the printer. Closes the connection to the printer.
""" """
if self.comm is not None: 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):
""" """
@ -90,152 +108,272 @@ class Printer():
Sends multiple gcode commands (provided as a list) to the printer. 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 setFeedrateModifier(self, structure, percentage): def setFeedrateModifier(self, structure, percentage):
if (not self.feedrateModifierMapping.has_key(structure)) or percentage < 0: if (not self._feedrateModifierMapping.has_key(structure)) or percentage < 0:
return return
self.comm.setFeedrateModifier(self.feedrateModifierMapping[structure], percentage / 100.0) self._comm.setFeedrateModifier(self._feedrateModifierMapping[structure], percentage / 100.0)
def loadGcode(self, file):
"""
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
self._setJobData(None, None, None)
self._gcodeLoader = GcodeLoader(file, self)
self._gcodeLoader.start()
def startPrint(self):
"""
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 is None:
return
if self._comm.isPrinting():
return
self._setCurrentZ(-1)
self._comm.printGCode(self._gcodeList)
def togglePausePrint(self):
"""
Pause the current printjob.
"""
if self._comm is None:
return
self._comm.setPause(not self._comm.isPaused())
def cancelPrint(self, disableMotorsAndHeater=True):
"""
Cancel the current printjob.
"""
if self._comm is None:
return
self._comm.cancelPrint()
if disableMotorsAndHeater:
self.commands(["M84", "M104 S0", "M140 S0"]) # disable motors, switch off heaters
# reset line, height, print time
self._setCurrentZ(None)
self._setProgressData(None, None, None)
#~~ state monitoring
def setTimelapse(self, timelapse): def setTimelapse(self, timelapse):
if self.timelapse is not None and self.isPrinting(): if self._timelapse is not None and self.isPrinting():
self.timelapse.onPrintjobStopped() self._timelapse.onPrintjobStopped()
del self.timelapse del self._timelapse
self.timelapse = timelapse self._timelapse = timelapse
def getTimelapse(self): def getTimelapse(self):
return self.timelapse return self._timelapse
def mcLog(self, message): def _setCurrentZ(self, currentZ):
self._currentZ = currentZ
for callback in self._callbacks:
try: callback.zChangeCB(self._currentZ)
except: pass
def _setState(self, state):
self._state = state
for callback in self._callbacks:
try: callback.stateChangeCB(self._state, self.getStateString(), self._getStateFlags())
except: pass
def _addLog(self, log):
""" """
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. Log line is stored in internal buffer, which is truncated to the last 300 lines.
""" """
self.log.append(message) self._latestLog = log
self.log = self.log[-300:] self._log.append(log)
self._log = self._log[-300:]
def mcTempUpdate(self, temp, bedTemp, targetTemp, bedTargetTemp): for callback in self._callbacks:
try: callback.logChangeCB(log, self._log)
except: pass
def _addMessage(self, message):
self._latestMessage = message
self._messages.append(message)
self._messages = self._messages[-300:]
for callback in self._callbacks:
try: callback.messageChangeCB(message, self._messages)
except: pass
def _setProgressData(self, progress, printTime, printTimeLeft):
self._progress = progress
self._printTime = printTime
self._printTimeLeft = printTimeLeft
for callback in self._callbacks:
try: callback.progressChangeCB(self._progress, self._printTime, self._printTimeLeft)
except: pass
def _addTemperatureData(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 information (actual and target) for print head and print bed is stored in corresponding
temperature history (including timestamp), history is truncated to 300 entries. 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._temp = temp
self.currentTargetTemp = targetTemp self._bedTemp = bedTemp
self.currentBedTemp = bedTemp self._targetTemp = targetTemp
self.currentBedTargetTemp = bedTargetTemp self._targetBedTemp = bedTargetTemp
for callback in self._callbacks:
try: callback.temperatureChangeCB(self._temp, self._bedTemp, self._targetTemp, self._targetBedTemp, self._temps)
except: pass
def _setJobData(self, filename, gcode, gcodeList):
self._filename = filename
self._gcode = gcode
self._gcodeList = gcodeList
for callback in self._callbacks:
try: callback.jobDataChangeCB(filename, len(gcodeList), self._gcode.totalMoveTimeMinute, self._gcode.extrusionAmount)
except: pass
def _sendInitialStateUpdate(self, callback):
lines = None
if self._gcodeList:
lines = len(self._gcodeList)
estimatedPrintTime = None
filament = None
if self._gcode:
estimatedPrintTime = self._gcode.totalMoveTimeMinute
filament = self._gcode.extrusionAmount
try:
callback.zChangeCB(self._currentZ)
callback.stateChangeCB(self._state, self.getStateString(), self._getStateFlags())
callback.logChangeCB(self._latestLog, self._log)
callback.messageChangeCB(self._latestMessage, self._messages)
callback.progressChangeCB(self._progress, self._printTime, self._printTimeLeft)
callback.temperatureChangeCB(self._temp, self._bedTemp, self._targetTemp, self._targetBedTemp, self._temps)
callback.jobDataChangeCB(self._filename, lines, estimatedPrintTime, filament)
except Exception, err:
import sys
sys.stderr.write("ERROR: %s\n" % str(err))
pass
def _getStateFlags(self):
return {
"operational": self.isOperational(),
"printing": self.isPrinting(),
"closedOrError": self.isClosedOrError(),
"error": self.isError(),
"loading": self.isLoading(),
"paused": self.isPaused(),
"ready": self.isReady()
}
#~~ callbacks triggered from self._comm
def mcLog(self, message):
"""
Callback method for the comm object, called upon log output.
"""
self._addLog(message)
def mcTempUpdate(self, temp, bedTemp, targetTemp, bedTargetTemp):
self._addTemperatureData(temp, bedTemp, targetTemp, bedTargetTemp)
def mcStateChange(self, state): def mcStateChange(self, state):
""" """
Callback method for the comm object, called if the connection state changes. Callback method for the comm object, called if the connection state changes.
New state is stored for retrieval by the frontend.
""" """
oldState = self.state oldState = self._state
self.state = state
if self._timelapse is not None:
if oldState == self._comm.STATE_PRINTING:
self._timelapse.onPrintjobStopped()
elif state == self._comm.STATE_PRINTING:
self._timelapse.onPrintjobStarted(self._filename)
self._setState(state)
if self.timelapse is not None:
if oldState == self.comm.STATE_PRINTING:
self.timelapse.onPrintjobStopped()
elif state == self.comm.STATE_PRINTING:
self.timelapse.onPrintjobStarted(self.filename)
def mcMessage(self, message): def mcMessage(self, message):
""" """
Callback method for the comm object, called upon message exchanges via serial. 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. Stores the message in the message buffer, truncates buffer to the last 300 lines.
""" """
self.messages.append(message) self._addMessage(message)
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. 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. Triggers storage of new values for printTime, printTimeLeft and the current line.
""" """
self.printTime = self.comm.getPrintTime() oldProgress = self._progress
self.printTimeLeft = self.comm.getPrintTimeRemainingEstimate()
oldProgress = self.progress; if self._timelapse is not None:
self.progress = self.comm.getPrintPos() try: self._timelapse.onPrintjobProgress(oldProgress, self._progress, int(round(self._progress * 100 / len(self._gcodeList))))
if self.timelapse is not None: except: pass
self.timelapse.onPrintjobProgress(oldProgress, self.progress, int(round(self.progress * 100 / len(self.gcodeList))))
self._setProgressData(self._comm.getPrintPos(), self._comm.getPrintTime(), self._comm.getPrintTimeRemainingEstimate())
def mcZChange(self, newZ): def mcZChange(self, newZ):
""" """
Callback method for the comm object, called upon change of the z-layer. Callback method for the comm object, called upon change of the z-layer.
""" """
oldZ = self.currentZ oldZ = self.currentZ
self.currentZ = newZ if self._timelapse is not None:
if self.timelapse is not None: self._timelapse.onZChange(oldZ, newZ)
self.timelapse.onZChange(oldZ, self.currentZ)
def onGcodeLoaded(self, gcodeLoader): self._setCurrentZ(newZ)
"""
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 #~~ callbacks triggered by gcodeLoader
def jobData(self): def onGcodeLoadingProgress(self, progress):
""" for callback in self._callbacks:
Returns statistics regarding the currently loaded printjob, or None if no printjob is loaded. try: callback.gcodeChangeCB(self._gcodeLoader._filename, progress)
""" except Exception, err:
if self.gcode is not None: import sys
formattedPrintTime = None sys.stderr.write("ERROR: %s\n" % str(err))
if (self.printTime): pass
formattedPrintTime = _getFormattedTimeDelta(datetime.timedelta(seconds=self.printTime))
formattedPrintTimeLeft = None def onGcodeLoaded(self):
if (self.printTimeLeft): self._setJobData(self._gcodeLoader._filename, self._gcodeLoader._gcode, self._gcodeLoader._gcodeList)
formattedPrintTimeLeft = _getFormattedTimeDelta(datetime.timedelta(minutes=self.printTimeLeft)) self._setCurrentZ(None)
self._setProgressData(None, None, None)
formattedPrintTimeEstimation = None self._gcodeLoader = None
formattedFilament = None
if self.gcode:
if self.gcode.totalMoveTimeMinute:
formattedPrintTimeEstimation = _getFormattedTimeDelta(datetime.timedelta(minutes=self.gcode.totalMoveTimeMinute))
if self.gcode.extrusionAmount:
formattedFilament = "%.2fm" % (self.gcode.extrusionAmount / 1000)
formattedCurrentZ = None for callback in self._callbacks:
if self.currentZ: try: callback.stateChangeCB(self._state, self.getStateString(), self._getStateFlags())
formattedCurrentZ = "%.2f mm" % (self.currentZ) except: pass
#~~ state reports
data = {
"filename": self.filename,
"currentZ": formattedCurrentZ,
"line": self.progress,
"totalLines": len(self.gcodeList),
"printTime": formattedPrintTime,
"printTimeLeft": formattedPrintTimeLeft,
"filament": formattedFilament,
"estimatedPrintTime": formattedPrintTimeEstimation
}
else:
data = None
return data
def gcodeState(self): def gcodeState(self):
if self.gcodeLoader is not None: if self.gcodeLoader is not None:
@ -247,12 +385,12 @@ class Printer():
return None return None
def feedrateState(self): def feedrateState(self):
if self.comm is not None: if self._comm is not None:
feedrateModifiers = self.comm.getFeedrateModifiers() feedrateModifiers = self._comm.getFeedrateModifiers()
result = {} result = {}
for structure in self.feedrateModifierMapping.keys(): for structure in self._feedrateModifierMapping.keys():
if (feedrateModifiers.has_key(self.feedrateModifierMapping[structure])): if (feedrateModifiers.has_key(self._feedrateModifierMapping[structure])):
result[structure] = int(round(feedrateModifiers[self.feedrateModifierMapping[structure]] * 100)) result[structure] = int(round(feedrateModifiers[self._feedrateModifierMapping[structure]] * 100))
else: else:
result[structure] = 100 result[structure] = 100
return result return result
@ -263,84 +401,31 @@ class Printer():
""" """
Returns a human readable string corresponding to the current communication state. Returns a human readable string corresponding to the current communication state.
""" """
if self.comm is None: if self._comm is None:
return "Offline" return "Offline"
else: else:
return self.comm.getStateString() return self._comm.getStateString()
def isClosedOrError(self): def isClosedOrError(self):
return self.comm is None or self.comm.isClosedOrError() return self._comm is None or self._comm.isClosedOrError()
def isOperational(self): def isOperational(self):
return self.comm is not None and self.comm.isOperational() return self._comm is not None and self._comm.isOperational()
def isPrinting(self): def isPrinting(self):
return self.comm is not None and self.comm.isPrinting() return self._comm is not None and self._comm.isPrinting()
def isPaused(self): def isPaused(self):
return self.comm is not None and self.comm.isPaused() return self._comm is not None and self._comm.isPaused()
def isError(self): def isError(self):
return self.comm is not None and self.comm.isError() return self._comm is not None and self._comm.isError()
def isReady(self): def isReady(self):
return self.gcodeLoader is None and self.gcodeList and len(self.gcodeList) > 0 return self._gcodeLoader is None and self._gcodeList and len(self._gcodeList) > 0
def isLoading(self): def isLoading(self):
return self.gcodeLoader is not None return self._gcodeLoader is not None
def loadGcode(self, file):
"""
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
self.filename = None
self.gcode = None
self.gcodeList = None
self.gcodeLoader = GcodeLoader(file, self)
self.gcodeLoader.start()
def startPrint(self):
"""
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 is None:
return
if self.comm.isPrinting():
return
self.currentZ = -1
self.comm.printGCode(self.gcodeList)
def togglePausePrint(self):
"""
Pause the current printjob.
"""
if self.comm is None:
return
self.comm.setPause(not self.comm.isPaused())
def cancelPrint(self, disableMotorsAndHeater=True):
"""
Cancel the current printjob.
"""
if self.comm is None:
return
self.comm.cancelPrint()
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): class GcodeLoader(Thread):
""" """
@ -352,19 +437,19 @@ class GcodeLoader(Thread):
def __init__(self, filename, printerCallback): def __init__(self, filename, printerCallback):
Thread.__init__(self); Thread.__init__(self);
self.printerCallback = printerCallback; self._printerCallback = printerCallback;
self.filename = filename self._filename = filename
self.progress = None self._progress = None
self.gcode = None self._gcode = None
self.gcodeList = None self._gcodeList = None
def run(self): def run(self):
#Send an initial M110 to reset the line counter to zero. #Send an initial M110 to reset the line counter to zero.
prevLineType = lineType = "CUSTOM" prevLineType = lineType = "CUSTOM"
gcodeList = ["M110"] gcodeList = ["M110"]
with open(self.filename, "r") as file: with open(self._filename, "r") as file:
for line in file: for line in file:
if line.startswith(";TYPE:"): if line.startswith(";TYPE:"):
lineType = line[6:].strip() lineType = line[6:].strip()
@ -378,13 +463,38 @@ class GcodeLoader(Thread):
gcodeList.append(line) gcodeList.append(line)
prevLineType = lineType prevLineType = lineType
self.gcodeList = gcodeList self._gcodeList = gcodeList
self.gcode = gcodeInterpreter.gcode() self._gcode = gcodeInterpreter.gcode()
self.gcode.progressCallback = self.onProgress self._gcode.progressCallback = self.onProgress
self.gcode.loadList(self.gcodeList) self._gcode.loadList(self._gcodeList)
self.printerCallback.onGcodeLoaded(self) self._printerCallback.onGcodeLoaded()
def onProgress(self, progress): def onProgress(self, progress):
self.progress = progress self._progress = progress
self._printerCallback.onGcodeLoadingProgress(progress)
class PrinterCallback(object):
def zChangeCB(self, newZ):
pass
def progressChangeCB(self, currentLine, printTime, printTimeLeft):
pass
def temperatureChangeCB(self, temp, bedTemp, targetTemp, bedTargetTemp, history):
pass
def stateChangeCB(self, state, stateString, booleanStates):
pass
def logChangeCB(self, line, history):
pass
def messageChangeCB(self, line, history):
pass
def gcodeChangeCB(self, filename, progress):
pass
def jobDataChangeCB(self, filename, lines, estimatedPrintTime, filamentLength):
pass

View File

@ -4,13 +4,16 @@ __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agp
from flask import Flask, request, render_template, jsonify, send_from_directory, abort, url_for from flask import Flask, request, render_template, jsonify, send_from_directory, abort, url_for
from werkzeug import secure_filename from werkzeug import secure_filename
import tornadio2
from printer_webui.printer import Printer, getConnectionOptions
from printer_webui.settings import settings
import timelapse
import os import os
import fnmatch import fnmatch
import datetime
import time
from printer_webui.printer import Printer, getConnectionOptions, PrinterCallback
from printer_webui.settings import settings
import printer_webui.timelapse as timelapse
BASEURL = "/ajax/" BASEURL = "/ajax/"
SUCCESS = {} SUCCESS = {}
@ -30,64 +33,78 @@ def index():
#~~ Printer state #~~ Printer state
@app.route(BASEURL + "state", methods=["GET"]) class PrinterStateConnection(tornadio2.SocketConnection, PrinterCallback):
def printerState(): def __init__(self, session, endpoint=None):
temp = printer.currentTemp tornadio2.SocketConnection.__init__(self, session, endpoint)
bedTemp = printer.currentBedTemp self._lastProgressReport = None
targetTemp = printer.currentTargetTemp
bedTargetTemp = printer.currentBedTargetTemp
jobData = printer.jobData()
gcodeState = printer.gcodeState()
feedrateState = printer.feedrateState()
result = { def on_open(self, info):
"state": printer.getStateString(), printer.registerCallback(self)
"temp": temp,
"bedTemp": bedTemp,
"targetTemp": targetTemp,
"targetBedTemp": bedTargetTemp,
"operational": printer.isOperational(),
"closedOrError": printer.isClosedOrError(),
"error": printer.isError(),
"printing": printer.isPrinting(),
"paused": printer.isPaused(),
"ready": printer.isReady(),
"loading": printer.isLoading()
}
if jobData is not None: def on_close(self):
jobData["filename"] = jobData["filename"].replace(UPLOAD_FOLDER + os.sep, "") printer.unregisterCallback(self)
result["job"] = jobData
if gcodeState is not None: def zChangeCB(self, currentZ):
gcodeState["filename"] = gcodeState["filename"].replace(UPLOAD_FOLDER + os.sep, "") formattedCurrentZ = None
result["gcode"] = gcodeState if currentZ:
formattedCurrentZ = "%.2f mm" % (currentZ)
if feedrateState is not None: self.emit("zChange", {"currentZ": formattedCurrentZ})
result["feedrate"] = feedrateState
if request.values.has_key("temperatures"): def progressChangeCB(self, currentLine, printTimeInSeconds, printTimeLeftInMinutes):
result["temperatures"] = printer.temps if self._lastProgressReport and time.time() + 0.5 < self._lastProgressReport:
return
if request.values.has_key("log"): formattedPrintTime = None
result["log"] = printer.log if (printTimeInSeconds):
formattedPrintTime = _getFormattedTimeDelta(datetime.timedelta(seconds=printTimeInSeconds))
if request.values.has_key("messages"): formattedPrintTimeLeft = None
result["messages"] = printer.messages if (printTimeLeftInMinutes):
formattedPrintTimeLeft = _getFormattedTimeDelta(datetime.timedelta(minutes=printTimeLeftInMinutes))
return jsonify(result) self._lastProgressReport = time.time()
self.emit("printProgress", {
"currentLine": currentLine,
"printTime": formattedPrintTime,
"printTimeLeft": formattedPrintTimeLeft
})
@app.route(BASEURL + "state/messages", methods=["GET"]) def temperatureChangeCB(self, temp, bedTemp, targetTemp, bedTargetTemp, history):
def printerMessages(): self.emit("temperature", {
return jsonify(messages=printer.messages) "currentTemp": temp,
"currentBedTemp": bedTemp,
"currentTargetTemp": targetTemp,
"currentTargetBedTemp": bedTargetTemp,
"history": history
})
@app.route(BASEURL + "state/log", methods=["GET"]) def stateChangeCB(self, state, stateString, booleanStates):
def printerLogs(): self.emit("state", {"currentState": stateString, "flags": booleanStates})
return jsonify(log=printer.log)
@app.route(BASEURL + "state/temperatures", methods=["GET"]) def logChangeCB(self, line, history):
def printerTemperatures(): self.emit("log", {"line": line, "history": history})
return jsonify(temperatures = printer.temps)
def messageChangeCB(self, line, history):
self.emit("message", {"line": line, "history": history})
def gcodeChangeCB(self, filename, progress):
self.emit("jobData", {"filename": "Loading... (%d%%)" % (round(progress * 100)), "lineCount": None, "estimatedPrintTime": None, "filament": None})
def jobDataChangeCB(self, filename, lines, estimatedPrintTimeInMinutes, filamentLengthInMillimeters):
formattedPrintTimeEstimation = None
if estimatedPrintTimeInMinutes:
formattedPrintTimeEstimation = _getFormattedTimeDelta(datetime.timedelta(minutes=estimatedPrintTimeInMinutes))
formattedFilament = None
if filamentLengthInMillimeters:
formattedFilament = "%.2fm" % (filamentLengthInMillimeters / 1000)
formattedFilename = None
if filename:
formattedFilename = filename.replace(UPLOAD_FOLDER + os.sep, "")
self.emit("jobData", {"filename": formattedFilename, "lineCount": lines, "estimatedPrintTime": formattedPrintTimeEstimation, "filament": formattedFilament})
#~~ Printer control #~~ Printer control
@ -315,6 +332,12 @@ def setSettings():
#~~ helper functions #~~ helper functions
def _getFormattedTimeDelta(d):
hours = d.seconds // 3600
minutes = (d.seconds % 3600) // 60
seconds = d.seconds % 60
return "%02d:%02d:%02d" % (hours, minutes, seconds)
def sizeof_fmt(num): 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
@ -331,8 +354,21 @@ def allowed_file(filename, extensions):
#~~ startup code #~~ startup code
def run(host = "0.0.0.0", port = 5000, debug = False): def run(host = "0.0.0.0", port = 5000, debug = False):
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)
app.debug = debug app.debug = debug
app.run(host=host, port=port, use_reloader=False)
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()
def main(): def main():
from optparse import OptionParser from optparse import OptionParser

View File

@ -122,3 +122,7 @@ table th.timelapse_files_action, table td.timelapse_files_action {
text-align: center; text-align: center;
width: 20%; width: 20%;
} }
#webcam_container {
width: 100%;
}

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -49,16 +49,16 @@ function ConnectionViewModel() {
self.saveSettings(false); self.saveSettings(false);
} }
self.fromStateResponse = function(response) { self.fromStateEvent = function(data) {
self.previousIsOperational = self.isOperational(); self.previousIsOperational = self.isOperational();
self.isErrorOrClosed(response.closedOrError); self.isErrorOrClosed(data.flags.closedOrError);
self.isOperational(response.operational); self.isOperational(data.flags.operational);
self.isPaused(response.paused); self.isPaused(data.flags.paused);
self.isPrinting(response.printing); self.isPrinting(data.flags.printing);
self.isError(response.error); self.isError(data.flags.error);
self.isReady(response.ready); self.isReady(data.flags.ready);
self.isLoading(response.loading); self.isLoading(data.flags.loading);
var connectionTab = $("#connection"); var connectionTab = $("#connection");
if (self.previousIsOperational != self.isOperational()) { if (self.previousIsOperational != self.isOperational()) {
@ -139,39 +139,36 @@ function PrinterStateViewModel() {
return "Pause"; return "Pause";
}); });
self.fromResponse = function(response) { self.fromStateEvent = function(data) {
self.stateString(response.state); self.stateString(data.currentState);
self.isErrorOrClosed(response.closedOrError); self.isErrorOrClosed(data.flags.closedOrError);
self.isOperational(response.operational); self.isOperational(data.flags.operational);
self.isPaused(response.paused); self.isPaused(data.flags.paused);
self.isPrinting(response.printing); self.isPrinting(data.flags.printing);
self.isError(response.error); self.isError(data.flags.error);
self.isReady(response.ready); self.isReady(data.flags.ready);
self.isLoading(response.loading); self.isLoading(data.flags.loading);
}
if (response.job) { self.fromJobEvent = function(data) {
self.filename(response.job.filename); self.filename(data.filename);
self.filament(response.job.filament); self.totalLines(data.lineCount);
self.estimatedPrintTime(response.job.estimatedPrintTime); self.estimatedPrintTime(data.estimatedPrintTime);
self.printTime(response.job.printTime); self.filament(data.filament);
self.printTimeLeft(response.job.printTimeLeft); }
self.currentLine(response.job.line ? response.job.line : 0);
self.totalLines(response.job.totalLines ? response.job.totalLines : 0); self.fromProgressEvent = function(data) {
self.currentHeight(response.job.currentZ); self.currentLine(data.currentLine);
} else { self.printTime(data.printTime);
if (response.loading && response.gcode) { self.printTimeLeft(data.printTimeLeft);
self.filename("Loading... (" + Math.round(response.gcode.progress * 100) + "%)"); }
} else {
self.filename(undefined); self.fromZChangeEvent = function(data) {
} self.currentHeight(data.currentZ);
self.filament(undefined); }
self.estimatedPrintTime(undefined);
self.printTime(undefined); self.fromGcodeEvent = function(data) {
self.printTimeLeft(undefined); self.filename("Loading... (" + Math.round(data.progress * 100) + "%)");
self.currentLine(undefined);
self.totalLines(undefined);
self.currentHeight(undefined);
}
} }
} }
var printerStateViewModel = new PrinterStateViewModel(); var printerStateViewModel = new PrinterStateViewModel();
@ -238,20 +235,22 @@ function TemperatureViewModel() {
} }
} }
self.fromResponse = function(response) { self.fromStateEvent = function(data) {
self.temp(response.temp); self.isErrorOrClosed(data.flags.closedOrError);
self.bedTemp(response.bedTemp); self.isOperational(data.flags.operational);
self.targetTemp(response.targetTemp); self.isPaused(data.flags.paused);
self.bedTargetTemp(response.bedTargetTemp); self.isPrinting(data.flags.printing);
self.temperatures = (response.temperatures); self.isError(data.flags.error);
self.isReady(data.flags.ready);
self.isLoading(data.flags.loading);
}
self.isErrorOrClosed(response.closedOrError); self.fromTemperatureEvent = function(data) {
self.isOperational(response.operational); self.temp(data.temp);
self.isPaused(response.paused); self.bedTemp(data.bedTemp);
self.isPrinting(response.printing); self.targetTemp(data.targetTemp);
self.isError(response.error); self.bedTargetTemp(data.bedTargetTemp);
self.isReady(response.ready); self.temperatures = (data.history);
self.isLoading(response.loading);
self.updatePlot(); self.updatePlot();
} }
@ -284,15 +283,16 @@ function SpeedViewModel() {
self.isReady = ko.observable(undefined); self.isReady = ko.observable(undefined);
self.isLoading = ko.observable(undefined); self.isLoading = ko.observable(undefined);
self.fromResponse = function(response) { self.fromStateEvent = function(data) {
self.isErrorOrClosed(response.closedOrError); self.isErrorOrClosed(data.closedOrError);
self.isOperational(response.operational); self.isOperational(data.operational);
self.isPaused(response.paused); self.isPaused(data.paused);
self.isPrinting(response.printing); self.isPrinting(data.printing);
self.isError(response.error); self.isError(data.error);
self.isReady(response.ready); self.isReady(data.ready);
self.isLoading(response.loading); self.isLoading(data.loading);
/*
if (response.feedrate) { if (response.feedrate) {
self.outerWall(response.feedrate.outerWall); self.outerWall(response.feedrate.outerWall);
self.innerWall(response.feedrate.innerWall); self.innerWall(response.feedrate.innerWall);
@ -304,6 +304,7 @@ function SpeedViewModel() {
self.fill(undefined); self.fill(undefined);
self.support(undefined); self.support(undefined);
} }
*/
} }
} }
var speedViewModel = new SpeedViewModel(); var speedViewModel = new SpeedViewModel();
@ -313,9 +314,26 @@ function TerminalViewModel() {
self.log = undefined; self.log = undefined;
self.fromResponse = function(response) { self.isErrorOrClosed = ko.observable(undefined);
self.log = response.log; 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.fromStateEvent = function(data) {
self.isErrorOrClosed(data.flags.closedOrError);
self.isOperational(data.flags.operational);
self.isPaused(data.flags.paused);
self.isPrinting(data.flags.printing);
self.isError(data.flags.error);
self.isReady(data.flags.ready);
self.isLoading(data.flags.loading);
}
self.fromLogEvent = function(data) {
self.log = data.history;
self.updateOutput(); self.updateOutput();
} }
@ -334,10 +352,6 @@ function TerminalViewModel() {
container.scrollTop(container[0].scrollHeight - container.height()) container.scrollTop(container[0].scrollHeight - container.height())
} }
} }
self.sendCommand = function(command) {
}
} }
var terminalViewModel = new TerminalViewModel(); var terminalViewModel = new TerminalViewModel();
@ -413,7 +427,7 @@ function WebcamViewModel() {
type: "GET", type: "GET",
dataType: "json", dataType: "json",
success: self.fromResponse success: self.fromResponse
}) });
} }
self.fromResponse = function(response) { self.fromResponse = function(response) {
@ -427,14 +441,14 @@ function WebcamViewModel() {
} }
} }
self.fromStateResponse = function(response) { self.fromStateEvent = function(data) {
self.isErrorOrClosed(response.closedOrError); self.isErrorOrClosed(data.flags.closedOrError);
self.isOperational(response.operational); self.isOperational(data.flags.operational);
self.isPaused(response.paused); self.isPaused(data.flags.paused);
self.isPrinting(response.printing); self.isPrinting(data.flags.printing);
self.isError(response.error); self.isError(data.flags.error);
self.isReady(response.ready); self.isReady(data.flags.ready);
self.isLoading(response.loading); self.isLoading(data.flags.loading);
} }
self.removeFile = function() { self.removeFile = function() {
@ -470,11 +484,6 @@ var webcamViewModel = new WebcamViewModel();
function DataUpdater(connectionViewModel, printerStateViewModel, temperatureViewModel, speedViewModel, terminalViewModel, webcamViewModel) { function DataUpdater(connectionViewModel, printerStateViewModel, temperatureViewModel, speedViewModel, terminalViewModel, webcamViewModel) {
var self = this; var self = this;
self.updateInterval = 500;
self.updateIntervalOnError = 10000;
self.includeTemperatures = true;
self.includeLogs = true;
self.connectionViewModel = connectionViewModel; self.connectionViewModel = connectionViewModel;
self.printerStateViewModel = printerStateViewModel; self.printerStateViewModel = printerStateViewModel;
self.temperatureViewModel = temperatureViewModel; self.temperatureViewModel = temperatureViewModel;
@ -482,6 +491,32 @@ function DataUpdater(connectionViewModel, printerStateViewModel, temperatureView
self.speedViewModel = speedViewModel; self.speedViewModel = speedViewModel;
self.webcamViewModel = webcamViewModel; self.webcamViewModel = webcamViewModel;
self.socket = io.connect();
self.socket.on("state", function(data) {
self.printerStateViewModel.fromStateEvent(data);
self.connectionViewModel.fromStateEvent(data);
self.temperatureViewModel.fromStateEvent(data);
self.terminalViewModel.fromStateEvent(data);
self.speedViewModel.fromStateEvent(data);
self.webcamViewModel.fromStateEvent(data);
})
self.socket.on("temperature", function(data) {
self.temperatureViewModel.fromTemperatureEvent(data);
})
self.socket.on("jobData", function(data) {
self.printerStateViewModel.fromJobEvent(data);
})
self.socket.on("log", function(data) {
self.terminalViewModel.fromLogEvent(data);
})
self.socket.on("printProgress", function(data) {
self.printerStateViewModel.fromProgressEvent(data);
})
self.socket.on("zChange", function(data) {
self.printerStateViewModel.fromZChangeEvent(data);
})
self.requestData = function() { self.requestData = function() {
var parameters = {}; var parameters = {};
@ -496,8 +531,10 @@ function DataUpdater(connectionViewModel, printerStateViewModel, temperatureView
dataType: "json", dataType: "json",
data: parameters, data: parameters,
success: function(response) { success: function(response) {
if ($("#offline_overlay").is(":visible")) if ($("#offline_overlay").is(":visible")) {
$("#offline_overlay").hide(); $("#offline_overlay").hide();
self.webcamViewModel.requestData();
}
self.printerStateViewModel.fromResponse(response); self.printerStateViewModel.fromResponse(response);
self.connectionViewModel.fromStateResponse(response); self.connectionViewModel.fromStateResponse(response);
@ -670,7 +707,7 @@ $(function() {
//~~ startup commands //~~ startup commands
dataUpdater.requestData(); //dataUpdater.requestData();
connectionViewModel.requestData(); connectionViewModel.requestData();
gcodeFilesViewModel.requestData(); gcodeFilesViewModel.requestData();
webcamViewModel.requestData(); webcamViewModel.requestData();

View File

@ -9,16 +9,11 @@
<link href="{{ url_for('static', filename='css/jquery.fileupload-ui.css') }}" rel="stylesheet" media="screen"> <link href="{{ url_for('static', filename='css/jquery.fileupload-ui.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='css/ui.css') }}" rel="stylesheet" media="screen"> <link href="{{ url_for('static', filename='css/ui.css') }}" rel="stylesheet" media="screen">
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script src="{{ url_for('static', filename='js/knockout-2.2.0.js') }}"></script>
<script src="{{ url_for('static', filename='js/bootstrap.js') }}"></script>
<script src="{{ url_for('static', filename='js/jquery.ui.widget.js') }}"></script>
<script src="{{ url_for('static', filename='js/jquery.flot.js') }}"></script>
<script src="{{ url_for('static', filename='js/jquery.iframe-transport.js') }}"></script>
<script src="{{ url_for('static', filename='js/jquery.fileupload.js') }}"></script>
<script src="{{ url_for('static', filename='js/ui.js') }}"></script>
<script lang="javascript"> <script lang="javascript">
var AJAX_BASEURL = '/ajax/'; var AJAX_BASEURL = '/ajax/';
var WEB_SOCKET_SWF_LOCATION = "{{ url_for('static', filename='js/WebSocketMain.swf') }}";
var WEB_SOCKET_DEBUG = true;
</script> </script>
</head> </head>
<body> <body>
@ -283,5 +278,15 @@
</div> </div>
</div> </div>
</div> </div>
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script src="{{ url_for('static', filename='js/knockout-2.2.0.js') }}"></script>
<script src="{{ url_for('static', filename='js/bootstrap.js') }}"></script>
<script src="{{ url_for('static', filename='js/jquery.ui.widget.js') }}"></script>
<script src="{{ url_for('static', filename='js/jquery.flot.js') }}"></script>
<script src="{{ url_for('static', filename='js/jquery.iframe-transport.js') }}"></script>
<script src="{{ url_for('static', filename='js/jquery.fileupload.js') }}"></script>
<script src="{{ url_for('static', filename='js/socket.io.js') }}"></script>
<script src="{{ url_for('static', filename='js/ui.js') }}"></script>
</body> </body>
</html> </html>