State is now sent in update bundles to the frontend if updates are available, but not more frequently than every 500ms.

master
Gina Häußge 2013-01-12 00:00:58 +01:00
parent 4983a00adf
commit e8a633db8b
4 changed files with 276 additions and 159 deletions

View File

@ -3,7 +3,10 @@ __author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
import time
from threading import Thread, Event, Lock
import datetime
import threading
import copy
import os
import printer_webui.util.comm as comm
from printer_webui.util import gcodeInterpreter
@ -71,9 +74,19 @@ class Printer():
self._callbacks = []
self._lastProgressReport = None
self._stateMonitor = StateMonitor(ratelimit=0.5, updateCallback=self._sendCurrentDataCallbacks)
self._stateMonitor = StateMonitor(
ratelimit=0.5,
updateCallback=self._sendCurrentDataCallbacks,
addTemperatureCallback=self._sendAddTemperatureCallbacks,
addLogCallback=self._sendAddLogCallbacks,
addMessageCallback=self._sendAddMessageCallbacks
)
self._stateMonitor.reset(
state={"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()}
state={"state": None, "stateString": self.getStateString(), "flags": self._getStateFlags()},
jobData={"filename": None, "lines": None, "estimatedPrintTime": None, "filament": None},
gcodeData={"filename": None, "progress": None},
progress={"progress": None, "printTime": None, "printTimeLeft": None},
currentZ=None
)
#~~ callback handling
@ -86,9 +99,24 @@ class Printer():
if callback in self._callbacks:
self._callbacks.remove(callback)
def _sendAddTemperatureCallbacks(self, data):
for callback in self._callbacks:
try: callback.addTemperature(data)
except: pass
def _sendAddLogCallbacks(self, data):
for callback in self._callbacks:
try: callback.addLog(data)
except: pass
def _sendAddMessageCallbacks(self, data):
for callback in self._callbacks:
try: callback.addMessage(data)
except: pass
def _sendCurrentDataCallbacks(self, data):
for callback in self._callbacks:
try: callback.sendCurrentData(data)
try: callback.sendCurrentData(copy.deepcopy(data))
except: pass
#~~ printer commands
@ -139,9 +167,11 @@ class Printer():
self._setJobData(None, None, None)
self._gcodeLoader = GcodeLoader(file, self)
self._gcodeLoader = GcodeLoader(file, self._onGcodeLoadingProgress, self._onGcodeLoaded)
self._gcodeLoader.start()
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
def startPrint(self):
"""
Starts the currently loaded print job.
@ -192,11 +222,15 @@ class Printer():
def _setCurrentZ(self, currentZ):
self._currentZ = currentZ
self._stateMonitor.setCurrentZ(self._currentZ)
formattedCurrentZ = None
if self._currentZ:
formattedCurrentZ = "%.2f mm" % (self._currentZ)
self._stateMonitor.setCurrentZ(formattedCurrentZ)
def _setState(self, state):
self._state = state
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "stateFlags": self._getStateFlags()})
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
def _addLog(self, log):
self._log.append(log)
@ -212,7 +246,16 @@ class Printer():
self._progress = progress
self._printTime = printTime
self._printTimeLeft = printTimeLeft
self._stateMonitor.setProgress({"progress": self._progress, "printTime": self._printTime, "printTimeLeft": self._printTimeLeft})
formattedPrintTime = None
if (self._printTime):
formattedPrintTime = _getFormattedTimeDelta(datetime.timedelta(seconds=self._printTime))
formattedPrintTimeLeft = None
if (self._printTimeLeft):
formattedPrintTimeLeft = _getFormattedTimeDelta(datetime.timedelta(minutes=self._printTimeLeft))
self._stateMonitor.setProgress({"progress": self._progress, "printTime": formattedPrintTime, "printTimeLeft": formattedPrintTimeLeft})
def _addTemperatureData(self, temp, bedTemp, targetTemp, bedTargetTemp):
currentTime = int(time.time() * 1000)
@ -245,25 +288,21 @@ class Printer():
if self._gcodeList:
lines = len(self._gcodeList)
estimatedPrintTime = None
filament = None
formattedPrintTimeEstimation = None
formattedFilament = None
if self._gcode:
estimatedPrintTime = self._gcode.totalMoveTimeMinute
filament = self._gcode.extrusionAmount
if self._gcode.totalMoveTimeMinute:
formattedPrintTimeEstimation = _getFormattedTimeDelta(datetime.timedelta(minutes=self._gcode.totalMoveTimeMinute))
if self._gcode.extrusionAmount:
formattedFilament = "%.2fm" % (self._gcode.extrusionAmount / 1000)
self._stateMonitor.setJobData({"filename": self._filename, "lines": lines, "estimatedPrintTime": estimatedPrintTime, "filament": filament})
formattedFilename = None
if self._filename:
formattedFilename = os.path.basename(self._filename)
self._stateMonitor.setJobData({"filename": formattedFilename, "lines": lines, "estimatedPrintTime": formattedPrintTimeEstimation, "filament": formattedFilament})
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:
data = self._stateMonitor.getCurrentData()
data.update({
@ -346,16 +385,24 @@ class Printer():
#~~ callbacks triggered by gcodeLoader
def onGcodeLoadingProgress(self, progress):
self._stateMonitor.setGcodeData({"filename": self._gcodeLoader._filename, "progress": progress})
def _onGcodeLoadingProgress(self, filename, progress):
formattedFilename = None
if filename is not None:
formattedFilename = os.path.basename(filename)
def onGcodeLoaded(self):
self._setJobData(self._gcodeLoader._filename, self._gcodeLoader._gcode, self._gcodeLoader._gcodeList)
self._stateMonitor.setGcodeData({"filename": formattedFilename, "progress": progress})
def _onGcodeLoaded(self, filename, gcode, gcodeList):
formattedFilename = None
if filename is not None:
formattedFilename = os.path.basename(filename)
self._setJobData(formattedFilename, gcode, gcodeList)
self._setCurrentZ(None)
self._setProgressData(None, None, None)
self._gcodeLoader = None
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "stateFlags": self._getStateFlags()})
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
#~~ state reports
@ -402,17 +449,18 @@ class Printer():
def isLoading(self):
return self._gcodeLoader is not None
class GcodeLoader(Thread):
class GcodeLoader(threading.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)
def __init__(self, filename, progressCallback, loadedCallback):
threading.Thread.__init__(self)
self._printerCallback = printerCallback
self._progressCallback = progressCallback
self._loadedCallback = loadedCallback
self._filename = filename
self._progress = None
@ -443,11 +491,11 @@ class GcodeLoader(Thread):
self._gcode.progressCallback = self.onProgress
self._gcode.loadList(self._gcodeList)
self._printerCallback.onGcodeLoaded()
self._loadedCallback(self._filename, self._gcode, self._gcodeList)
def onProgress(self, progress):
self._progress = progress
self._printerCallback.onGcodeLoadingProgress(progress)
self._progressCallback(self._filename, self._progress)
class PrinterCallback(object):
def sendCurrentData(self, data):
@ -456,48 +504,52 @@ class PrinterCallback(object):
def sendHistoryData(self, data):
pass
def addTemperature(self, data):
pass
def addLog(self, data):
pass
def addMessage(self, data):
pass
class StateMonitor(object):
def __init__(self, ratelimit, updateCallback):
def __init__(self, ratelimit, updateCallback, addTemperatureCallback, addLogCallback, addMessageCallback):
self._ratelimit = ratelimit
self._updateCallback = updateCallback
self._addTemperatureCallback = addTemperatureCallback
self._addLogCallback = addLogCallback
self._addMessageCallback = addMessageCallback
self._state = None
self._jobData = None
self._gcodeData = None
self._currentZ = None
self._progress = None
self._logBacklog = []
self._logHistory = []
self._messageBacklog = []
self._messageHistory = []
self._temperatureBacklog = []
self._temperatureHistory = []
self._temperatureBacklogMutex = Lock()
self._logBacklogMutex = Lock()
self._messageBacklogMutex = Lock()
self._changeEvent = Event()
self._changeEvent = threading.Event()
self._lastUpdate = time.time()
self._worker = Thread(target=self._work)
self._worker = threading.Thread(target=self._work)
self._worker.start()
def reset(self, state=None):
def reset(self, state=None, jobData=None, gcodeData=None, progress=None, currentZ=None):
self.setState(state)
self.setJobData(jobData)
self.setGcodeData(gcodeData)
self.setProgress(progress)
self.setCurrentZ(currentZ)
def addTemperature(self, temperature):
with self._temperatureBacklogMutex:
self._temperatureBacklog.append(temperature)
self._addTemperatureCallback(temperature)
self._changeEvent.set()
def addLog(self, log):
with self._logBacklogMutex:
self._logBacklog.append(log)
self._addLogCallback(log)
self._changeEvent.set()
def addMessage(self, message):
with self._messageBacklogMutex:
self._messageBacklog.append(message)
self._addMessageCallback(message)
self._changeEvent.set()
def setCurrentZ(self, currentZ):
@ -523,28 +575,14 @@ class StateMonitor(object):
def _work(self):
while True:
self._changeEvent.wait()
additionalWaitTime = time.time() + self._ratelimit - self._lastUpdate
now = time.time()
delta = now - self._lastUpdate
additionalWaitTime = self._ratelimit - delta
if additionalWaitTime > 0:
time.sleep(additionalWaitTime)
with self._temperatureBacklogMutex:
temperatures = self._temperatureBacklog
self._temperatureBacklog = []
with self._logBacklogMutex:
logs = self._logBacklog
self._logBacklog = []
with self._messageBacklogMutex:
messages = self._messageBacklog
self._messageBacklog = []
data = self.getCurrentData()
data.update({
"temperatures": temperatures,
"logs": logs,
"messages": messages
})
self._updateCallback(data)
self._lastUpdate = time.time()
self._changeEvent.clear()
@ -554,5 +592,14 @@ class StateMonitor(object):
"state": self._state,
"job": self._jobData,
"gcode": self._gcodeData,
"currentZ": self._currentZ
"currentZ": self._currentZ,
"progress": self._progress
}
def _getFormattedTimeDelta(d):
if d is None:
return None
hours = d.seconds // 3600
minutes = (d.seconds % 3600) // 60
seconds = d.seconds % 60
return "%02d:%02d:%02d" % (hours, minutes, seconds)

View File

@ -8,7 +8,7 @@ import tornadio2
import os
import fnmatch
import datetime
import threading
from printer_webui.printer import Printer, getConnectionOptions, PrinterCallback
from printer_webui.settings import settings
@ -33,25 +33,63 @@ def index():
#~~ Printer state
class PrinterStateConnection(tornadio2.SocketConnection, PrinterCallback):
def __init__(self, session, endpoint=None):
tornadio2.SocketConnection.__init__(self, session, endpoint)
self._temperatureBacklog = []
self._temperatureBacklogMutex = threading.Lock()
self._logBacklog = []
self._logBacklogMutex = threading.Lock()
self._messageBacklog = []
self._messageBacklogMutex = threading.Lock()
def on_open(self, info):
print("Opened socket")
print("New connection from client")
printer.registerCallback(self)
def on_close(self):
print("Closed socket")
print("Closed client connection")
printer.unregisterCallback(self)
def on_message(self, message):
pass
def sendCurrentData(self, data):
print("Sending current data...")
# add current temperature, log and message backlogs to sent data
with self._temperatureBacklogMutex:
temperatures = self._temperatureBacklog
self._temperatureBacklog = []
with self._logBacklogMutex:
logs = self._logBacklog
self._logBacklog = []
with self._messageBacklogMutex:
messages = self._messageBacklog
self._messageBacklog = []
data.update({
"temperatures": temperatures,
"logs": logs,
"messages": messages
})
self.emit("current", data)
def sendHistoryData(self, data):
print("Sending history...")
self.emit("history", data)
def addLog(self, data):
with self._logBacklogMutex:
self._logBacklog.append(data)
def addMessage(self, data):
with self._messageBacklogMutex:
self._messageBacklog.append(data)
def addTemperature(self, data):
with self._temperatureBacklogMutex:
self._temperatureBacklog.append(data)
#~~ Printer control
@app.route(BASEURL + "control/connectionOptions", methods=["GET"])
@ -278,12 +316,6 @@ def setSettings():
#~~ 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):
"""
Taken from http://stackoverflow.com/questions/1094841/reusable-library-to-get-human-readable-version-of-file-size

View File

@ -49,6 +49,10 @@ function ConnectionViewModel() {
self.saveSettings(false);
}
self.fromHistoryData = function(data) {
self._processStateData(data.state);
}
self.fromCurrentData = function(data) {
self._processStateData(data.state);
}
@ -102,7 +106,6 @@ function ConnectionViewModel() {
}
}
}
var connectionViewModel = new ConnectionViewModel();
function PrinterStateViewModel() {
var self = this;
@ -144,17 +147,21 @@ function PrinterStateViewModel() {
});
self.fromCurrentData = function(data) {
self._processStateData(data.state);
self._fromData(data);
}
self.fromHistoryData = function(data) {
self._fromData(data);
}
self._fromData = function(data) {
self._processStateData(data.state)
self._processJobData(data.job);
self._processGcodeData(data.gcode);
self._processProgressData(data.progress);
self._processZData(data.currentZ);
}
self.fromHistoryData = function(data) {
self._processStateData(data.state)
}
self._processStateData = function(data) {
self.stateString(data.stateString);
self.isErrorOrClosed(data.flags.closedOrError);
@ -168,7 +175,7 @@ function PrinterStateViewModel() {
self._processJobData = function(data) {
self.filename(data.filename);
self.totalLines(data.lineCount);
self.totalLines(data.lines);
self.estimatedPrintTime(data.estimatedPrintTime);
self.filament(data.filament);
}
@ -180,16 +187,15 @@ function PrinterStateViewModel() {
}
self._processProgressData = function(data) {
self.currentLine(data.currentLine);
self.currentLine(data.progress);
self.printTime(data.printTime);
self.printTimeLeft(data.printTimeLeft);
}
self._processZData = function(data) {
self.currentHeight(data.currentZ);
self.currentHeight(data);
}
}
var printerStateViewModel = new PrinterStateViewModel();
function TemperatureViewModel() {
var self = this;
@ -304,15 +310,15 @@ function TemperatureViewModel() {
self.temperatures.actualBed = self.temperatures.actualBed.slice(-300);
self.temperatures.targetBed = self.temperatures.targetBed.slice(-300);
self._updatePlot();
self.updatePlot();
}
self._processTemperatureHistoryData = function(data) {
self.temperatures = data;
self._updatePlot();
self.updatePlot();
}
self._updatePlot = function() {
self.updatePlot = function() {
var data = [
{label: "Actual", color: "#FF4040", data: self.temperatures.actual},
{label: "Target", color: "#FFA0A0", data: self.temperatures.target},
@ -322,7 +328,6 @@ function TemperatureViewModel() {
$.plot($("#temperature-graph"), data, self.plotOptions);
}
}
var temperatureViewModel = new TemperatureViewModel();
function SpeedViewModel() {
var self = this;
@ -340,31 +345,38 @@ function SpeedViewModel() {
self.isReady = ko.observable(undefined);
self.isLoading = ko.observable(undefined);
self.fromStateEvent = function(data) {
self.isErrorOrClosed(data.closedOrError);
self.isOperational(data.operational);
self.isPaused(data.paused);
self.isPrinting(data.printing);
self.isError(data.error);
self.isReady(data.ready);
self.isLoading(data.loading);
/*
if (response.feedrate) {
self.outerWall(response.feedrate.outerWall);
self.innerWall(response.feedrate.innerWall);
self.fill(response.feedrate.fill);
self.support(response.feedrate.support);
} else {
self.outerWall(undefined);
self.innerWall(undefined);
self.fill(undefined);
self.support(undefined);
}
*/
self._fromCurrentData = function(data) {
self._processStateData(data.state);
}
self._fromHistoryData = function(data) {
self._processStateData(data.state);
}
self._processStateData = 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);
}
/*
if (response.feedrate) {
self.outerWall(response.feedrate.outerWall);
self.innerWall(response.feedrate.innerWall);
self.fill(response.feedrate.fill);
self.support(response.feedrate.support);
} else {
self.outerWall(undefined);
self.innerWall(undefined);
self.fill(undefined);
self.support(undefined);
}
*/
}
var speedViewModel = new SpeedViewModel();
function TerminalViewModel() {
var self = this;
@ -379,7 +391,29 @@ function TerminalViewModel() {
self.isReady = ko.observable(undefined);
self.isLoading = ko.observable(undefined);
self.fromStateEvent = function(data) {
self.fromCurrentData = function(data) {
self._processStateData(data.state);
self._processCurrentLogData(data.logs);
}
self.fromHistoryData = function(data) {
self._processStateData(data.state);
self._processHistoryLogData(data.logHistory);
}
self._processCurrentLogData = function(data) {
if (!self.log)
self.log = []
self.log = self.log.concat(data)
self.updateOutput();
}
self._processHistoryLogData = function(data) {
self.log = data;
self.updateOutput();
}
self._processStateData = function(data) {
self.isErrorOrClosed(data.flags.closedOrError);
self.isOperational(data.flags.operational);
self.isPaused(data.flags.paused);
@ -389,18 +423,6 @@ function TerminalViewModel() {
self.isLoading(data.flags.loading);
}
self.fromLogEvent = function(data) {
if (!self.log)
self.log = []
self.log.concat(data.line)
self.updateOutput();
}
self.fromHistoryEvent = function(data) {
self.log = data;
self.updateOutput();
}
self.updateOutput = function() {
if (!self.log)
return;
@ -420,7 +442,6 @@ function TerminalViewModel() {
}
}
}
var terminalViewModel = new TerminalViewModel();
function GcodeFilesViewModel() {
var self = this;
@ -463,7 +484,6 @@ function GcodeFilesViewModel() {
})
}
}
var gcodeFilesViewModel = new GcodeFilesViewModel();
function WebcamViewModel() {
var self = this;
@ -508,7 +528,15 @@ function WebcamViewModel() {
}
}
self.fromStateEvent = function(data) {
self.fromCurrentData = function(data) {
self._processStateData(data.state);
}
self.fromHistoryData = function(data) {
self._processStateData(data.state);
}
self._processStateData = function(data) {
self.isErrorOrClosed(data.flags.closedOrError);
self.isOperational(data.flags.operational);
self.isPaused(data.flags.paused);
@ -546,7 +574,6 @@ function WebcamViewModel() {
})
}
}
var webcamViewModel = new WebcamViewModel();
function DataUpdater(connectionViewModel, printerStateViewModel, temperatureViewModel, speedViewModel, terminalViewModel, webcamViewModel) {
var self = this;
@ -558,41 +585,50 @@ function DataUpdater(connectionViewModel, printerStateViewModel, temperatureView
self.speedViewModel = speedViewModel;
self.webcamViewModel = webcamViewModel;
self.socket = io.connect();
self.socket.on("connect", function() {
self._socket = io.connect();
self._socket.on("connect", function() {
if ($("#offline_overlay").is(":visible")) {
$("#offline_overlay").hide();
self.webcamViewModel.requestData();
}
})
self.socket.on("disconnect", function() {
self._socket.on("disconnect", function() {
// if the updated fails to communicate with the backend, we interpret this as a missing backend
if (!$("#offline_overlay").is(":visible"))
$("#offline_overlay").show();
})
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("history", function(data) {
self._socket.on("history", function(data) {
self.connectionViewModel.fromHistoryData(data);
self.printerStateViewModel.fromHistoryData(data);
self.temperatureViewModel.fromHistoryData(data);
//self.terminalViewModel.fromHistoryData(data);
self.terminalViewModel.fromHistoryData(data);
self.webcamViewModel.fromHistoryData(data);
})
self.socket.on("current", function(data) {
self._socket.on("current", function(data) {
self.connectionViewModel.fromCurrentData(data);
self.printerStateViewModel.fromCurrentData(data);
self.temperatureViewModel.fromCurrentData(data);
self.terminalViewModel.fromCurrentData(data);
self.webcamViewModel.fromCurrentData(data);
})
self.reconnect = function() {
self._socket.socket.connect();
}
}
var dataUpdater = new DataUpdater(connectionViewModel, printerStateViewModel, temperatureViewModel, speedViewModel, terminalViewModel, webcamViewModel);
$(function() {
//~~ View models
var connectionViewModel = new ConnectionViewModel();
var printerStateViewModel = new PrinterStateViewModel();
var temperatureViewModel = new TemperatureViewModel();
var speedViewModel = new SpeedViewModel();
var terminalViewModel = new TerminalViewModel();
var gcodeFilesViewModel = new GcodeFilesViewModel();
var webcamViewModel = new WebcamViewModel();
var dataUpdater = new DataUpdater(connectionViewModel, printerStateViewModel, temperatureViewModel, speedViewModel, terminalViewModel, webcamViewModel);
//~~ Print job control
$("#job_print").click(function() {
@ -641,6 +677,9 @@ $(function() {
success: function() {$("#temp_newBedTemp").val("")}
})
})
$('#tabs a[data-toggle="tab"]').on('shown', function (e) {
temperatureViewModel.updatePlot();
});
//~~ Jog controls
@ -719,7 +758,7 @@ $(function() {
});
//~~ Offline overlay
$("#offline_overlay_reconnect").click(function() {dataUpdater.requestData()});
$("#offline_overlay_reconnect").click(function() {dataUpdater.reconnect()});
//~~ knockout.js bindings
@ -738,7 +777,6 @@ $(function() {
//~~ startup commands
//dataUpdater.requestData();
connectionViewModel.requestData();
gcodeFilesViewModel.requestData();
webcamViewModel.requestData();

View File

@ -105,11 +105,11 @@
</div>
<div class="tabbable span8">
<ul class="nav nav-tabs">
<li class="active"><a href="#temp" data-toggle="tab">Temp</a></li>
<li><a href="#jog" data-toggle="tab">Jog</a></li>
<li><a href="#speed" data-toggle="tab">Speed</a></li>
<li><a href="#term" data-toggle="tab">Term</a></li>
<ul class="nav nav-tabs" id="tabs">
<li class="active"><a href="#temp" data-toggle="tab">Temperature</a></li>
<li><a href="#jog" data-toggle="tab">Controls</a></li>
<!--<li><a href="#speed" data-toggle="tab">Speed</a></li>-->
<li><a href="#term" data-toggle="tab">Terminal</a></li>
{% if webcamStream %}<li><a href="#webcam" data-toggle="tab">Webcam</a></li>{% endif %}
</ul>