From e8a633db8be2c8d1b89c4a0b4236f6d232b6ab0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Sat, 12 Jan 2013 00:00:58 +0100 Subject: [PATCH] State is now sent in update bundles to the frontend if updates are available, but not more frequently than every 500ms. --- printer_webui/printer.py | 191 ++++++++++++++++++----------- printer_webui/server.py | 54 ++++++-- printer_webui/static/js/ui.js | 180 ++++++++++++++++----------- printer_webui/templates/index.html | 10 +- 4 files changed, 276 insertions(+), 159 deletions(-) diff --git a/printer_webui/printer.py b/printer_webui/printer.py index 85a459c..31237b8 100644 --- a/printer_webui/printer.py +++ b/printer_webui/printer.py @@ -3,7 +3,10 @@ __author__ = "Gina Häußge " __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) diff --git a/printer_webui/server.py b/printer_webui/server.py index 5c0f2ec..3b51dc4 100644 --- a/printer_webui/server.py +++ b/printer_webui/server.py @@ -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 diff --git a/printer_webui/static/js/ui.js b/printer_webui/static/js/ui.js index 7def828..b091e0d 100644 --- a/printer_webui/static/js/ui.js +++ b/printer_webui/static/js/ui.js @@ -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(); diff --git a/printer_webui/templates/index.html b/printer_webui/templates/index.html index cab51c4..587ec0f 100644 --- a/printer_webui/templates/index.html +++ b/printer_webui/templates/index.html @@ -105,11 +105,11 @@
-