# coding=utf-8 __author__ = "Gina Häußge " __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' import time import datetime import threading import copy import os #import logging, logging.config import octoprint.util.comm as comm import octoprint.util as util from octoprint.settings import settings from octoprint.events import eventManager def getConnectionOptions(): """ Retrieves the available ports, baudrates, prefered port and baudrate for connecting to the printer. """ return { "ports": comm.serialList(), "baudrates": comm.baudrateList(), "portPreference": settings().get(["serial", "port"]), "baudratePreference": settings().getInt(["serial", "baudrate"]), "autoconnect": settings().getBoolean(["serial", "autoconnect"]) } class Printer(): def __init__(self, gcodeManager): from collections import deque self._gcodeManager = gcodeManager # state self._temp = None self._bedTemp = None self._targetTemp = None self._targetBedTemp = None self._temps = { "actual": deque([], 300), "target": deque([], 300), "actualBed": deque([], 300), "targetBed": deque([], 300) } self._tempBacklog = [] self._latestMessage = None self._messages = deque([], 300) self._messageBacklog = [] self._latestLog = None self._log = deque([], 300) self._logBacklog = [] self._state = None self._currentZ = None self._progress = None self._printTime = None self._printTimeLeft = None self._printAfterSelect = False # sd handling self._sdPrinting = False self._sdStreaming = False self._selectedFile = None # comm self._comm = None # callbacks self._callbacks = [] self._lastProgressReport = None self._stateMonitor = StateMonitor( ratelimit=0.5, updateCallback=self._sendCurrentDataCallbacks, addTemperatureCallback=self._sendAddTemperatureCallbacks, addLogCallback=self._sendAddLogCallbacks, addMessageCallback=self._sendAddMessageCallbacks ) self._stateMonitor.reset( state={"state": None, "stateString": self.getStateString(), "flags": self._getStateFlags()}, jobData={"filename": None, "filesize": None, "estimatedPrintTime": None, "filament": None}, progress={"progress": None, "filepos": None, "printTime": None, "printTimeLeft": None}, currentZ=None ) #~~ callback handling def registerCallback(self, callback): self._callbacks.append(callback) self._sendInitialStateUpdate(callback) def unregisterCallback(self, callback): 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(copy.deepcopy(data)) except: pass def _sendTriggerUpdateCallbacks(self, type): for callback in self._callbacks: try: callback.sendUpdateTrigger(type) except: pass def _sendFeedbackCommandOutput(self, name, output): for callback in self._callbacks: try: callback.sendFeedbackCommandOutput(name, output) except: pass #~~ printer commands def connect(self, port=None, baudrate=None): """ Connects to the printer. If port and/or baudrate is provided, uses these settings, otherwise autodetection will be attempted. """ if self._comm is not None: self._comm.close() self._comm = comm.MachineCom(port, baudrate, callbackObject=self) def disconnect(self): """ Closes the connection to the printer. """ if self._comm is not None: self._comm.close() self._comm = None eventManager().fire("Disconnected") def command(self, command): """ Sends a single gcode command to the printer. """ self.commands([command]) def commands(self, commands): """ Sends multiple gcode commands (provided as a list) to the printer. """ for command in commands: self._comm.sendCommand(command) def selectFile(self, filename, sd, printAfterSelect=False): if self._comm is not None and (self._comm.isBusy() or self._comm.isStreaming()): return self._printAfterSelect = printAfterSelect self._comm.selectFile(filename, sd) 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() or self._comm.isPrinting(): return if self._selectedFile is None: return self._setCurrentZ(None) self._comm.startPrint() 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", "M106 S0"]) # disable motors, switch off heaters and fan # reset progress, height, print time self._setCurrentZ(None) self._setProgressData(None, None, None, None) # mark print as failure if self._selectedFile is not None: self._gcodeManager.printFailed(self._selectedFile["filename"]) eventManager().fire("PrintFailed", self._selectedFile["filename"]) #~~ state monitoring def _setCurrentZ(self, currentZ): self._currentZ = 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(), "flags": self._getStateFlags()}) def _addLog(self, log): self._log.append(log) self._stateMonitor.addLog(log) def _addMessage(self, message): self._messages.append(message) self._stateMonitor.addMessage(message) def _setProgressData(self, progress, filepos, printTime, printTimeLeft): self._progress = progress self._printTime = printTime self._printTimeLeft = printTimeLeft formattedPrintTime = None if (self._printTime): formattedPrintTime = util.getFormattedTimeDelta(datetime.timedelta(seconds=self._printTime)) formattedPrintTimeLeft = None if (self._printTimeLeft): formattedPrintTimeLeft = util.getFormattedTimeDelta(datetime.timedelta(minutes=self._printTimeLeft)) formattedFilePos = None if (filepos): formattedFilePos = util.getFormattedSize(filepos) self._stateMonitor.setProgress({"progress": self._progress, "filepos": formattedFilePos, "printTime": formattedPrintTime, "printTimeLeft": formattedPrintTimeLeft}) def _addTemperatureData(self, temp, bedTemp, targetTemp, bedTargetTemp): currentTimeUtc = int(time.time() * 1000) self._temps["actual"].append((currentTimeUtc, temp)) self._temps["target"].append((currentTimeUtc, targetTemp)) self._temps["actualBed"].append((currentTimeUtc, bedTemp)) self._temps["targetBed"].append((currentTimeUtc, bedTargetTemp)) self._temp = temp self._bedTemp = bedTemp self._targetTemp = targetTemp self._targetBedTemp = bedTargetTemp self._stateMonitor.addTemperature({"currentTime": currentTimeUtc, "temp": self._temp, "bedTemp": self._bedTemp, "targetTemp": self._targetTemp, "targetBedTemp": self._targetBedTemp}) def _setJobData(self, filename, filesize, sd): if filename is not None: self._selectedFile = { "filename": filename, "filesize": filesize, "sd": sd } else: self._selectedFile = None formattedFilename = None formattedFilesize = None estimatedPrintTime = None filament = None if filename: formattedFilename = os.path.basename(filename) if filesize: formattedFilesize = util.getFormattedSize(filesize) fileData = self._gcodeManager.getFileData(filename) if fileData is not None and "gcodeAnalysis" in fileData.keys(): if "estimatedPrintTime" in fileData["gcodeAnalysis"].keys(): estimatedPrintTime = fileData["gcodeAnalysis"]["estimatedPrintTime"] if "filament" in fileData["gcodeAnalysis"].keys(): filament = fileData["gcodeAnalysis"]["filament"] self._stateMonitor.setJobData({"filename": formattedFilename, "filesize": formattedFilesize, "estimatedPrintTime": estimatedPrintTime, "filament": filament, "sd": sd}) def _sendInitialStateUpdate(self, callback): try: data = self._stateMonitor.getCurrentData() data.update({ "temperatureHistory": list(self._temps), "logHistory": list(self._log), "messageHistory": list(self._messages) }) callback.sendHistoryData(data) except Exception, err: import sys sys.stderr.write("ERROR: %s\n" % str(err)) pass def _getStateFlags(self): if not settings().getBoolean(["feature", "sdSupport"]) or self._comm is None: sdReady = False else: sdReady = self._comm.isSdReady() return { "operational": self.isOperational(), "printing": self.isPrinting(), "closedOrError": self.isClosedOrError(), "error": self.isError(), "paused": self.isPaused(), "ready": self.isReady(), "sdReady": sdReady } def getCurrentData(self): return self._stateMonitor.getCurrentData() #~~ 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): """ Callback method for the comm object, called if the connection state changes. """ oldState = self._state # forward relevant state changes to gcode manager if self._comm is not None and oldState == self._comm.STATE_PRINTING: if self._selectedFile is not None: if state == self._comm.STATE_OPERATIONAL: self._gcodeManager.printSucceeded(self._selectedFile["filename"]) elif state == self._comm.STATE_CLOSED or state == self._comm.STATE_ERROR or state == self._comm.STATE_CLOSED_WITH_ERROR: self._gcodeManager.printFailed(self._selectedFile["filename"]) self._gcodeManager.resumeAnalysis() # printing done, put those cpu cycles to good use elif self._comm is not None and state == self._comm.STATE_PRINTING: self._gcodeManager.pauseAnalysis() # do not analyse gcode while printing self._setState(state) def mcMessage(self, message): """ Callback method for the comm object, called upon message exchanges via serial. Stores the message in the message buffer, truncates buffer to the last 300 lines. """ self._addMessage(message) def mcProgress(self): """ 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 progress. """ self._setProgressData(self._comm.getPrintProgress(), self._comm.getPrintFilepos(), self._comm.getPrintTime(), self._comm.getPrintTimeRemainingEstimate()) def mcZChange(self, newZ): """ Callback method for the comm object, called upon change of the z-layer. """ oldZ = self._currentZ if newZ != oldZ: # we have to react to all z-changes, even those that might "go backward" due to a slicer's retraction or # anti-backlash-routines. Event subscribes should individually take care to filter out "wrong" z-changes eventManager().fire("ZChange", newZ) self._setCurrentZ(newZ) def mcSdStateChange(self, sdReady): self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()}) def mcSdFiles(self, files): self._sendTriggerUpdateCallbacks("gcodeFiles") def mcFileSelected(self, filename, filesize, sd): self._setJobData(filename, filesize, sd) self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()}) if self._printAfterSelect: self.startPrint() def mcPrintjobDone(self): self._setProgressData(1.0, self._selectedFile["filesize"], self._comm.getPrintTime(), 0) self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()}) def mcFileTransferStarted(self, filename, filesize): self._sdStreaming = True self._selectedFile = { "filename": filename, "filesize": filesize, "sd": True } self._setJobData(filename, filesize, True) self._setProgressData(0.0, 0, 0, None) self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()}) def mcFileTransferDone(self): self._sdStreaming = False self._selectedFile = None self._setCurrentZ(None) self._setJobData(None, None, None) self._setProgressData(None, None, None, None) self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()}) def mcReceivedRegisteredMessage(self, command, output): self._sendFeedbackCommandOutput(command, output) #~~ sd file handling def getSdFiles(self): if self._comm is None: return return self._comm.getSdFiles() def addSdFile(self, filename, path): if not self._comm or self._comm.isBusy(): return self._comm.startFileTransfer(path, filename[:8].lower() + ".gco") def deleteSdFile(self, filename): if not self._comm: return self._comm.deleteSdFile(filename) def initSdCard(self): if not self._comm: return self._comm.initSdCard() def releaseSdCard(self): if not self._comm: return self._comm.releaseSdCard() def refreshSdFiles(self): if not self._comm: return self._comm.refreshSdFiles() #~~ state reports def getStateString(self): """ Returns a human readable string corresponding to the current communication state. """ if self._comm is None: return "Offline" else: return self._comm.getStateString() def getCurrentData(self): return self._stateMonitor.getCurrentData() def getCurrentJob(self): currentData = self._stateMonitor.getCurrentData() return currentData["job"] def getCurrentTemperatures(self): return { "extruder": { "current": self._temp, "target": self._targetTemp }, "bed": { "current": self._bedTemp, "target": self._targetBedTemp } } def isClosedOrError(self): return self._comm is None or self._comm.isClosedOrError() def isOperational(self): return self._comm is not None and self._comm.isOperational() def isPrinting(self): return self._comm is not None and self._comm.isPrinting() def isPaused(self): return self._comm is not None and self._comm.isPaused() def isError(self): return self._comm is not None and self._comm.isError() def isReady(self): return self.isOperational() and not self._comm.isStreaming() def isLoading(self): return self._gcodeLoader is not None 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, progressCallback, loadedCallback): threading.Thread.__init__(self) self._progressCallback = progressCallback self._loadedCallback = loadedCallback self._filename = filename self._gcodeList = None def run(self): #Send an initial M110 to reset the line counter to zero. prevLineType = lineType = "CUSTOM" gcodeList = ["M110 N0"] filesize = os.stat(self._filename).st_size 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._onLoadingProgress(float(file.tell()) / float(filesize)) self._gcodeList = gcodeList self._loadedCallback(self._filename, self._gcodeList) def _onLoadingProgress(self, progress): self._progressCallback(self._filename, progress, "loading") def _onParsingProgress(self, progress): self._progressCallback(self._filename, progress, "parsing") class SdFileStreamer(threading.Thread): def __init__(self, comm, filename, file, progressCallback, finishCallback): threading.Thread.__init__(self) self._comm = comm self._filename = filename self._file = file self._progressCallback = progressCallback self._finishCallback = finishCallback def run(self): if self._comm.isBusy(): return name = self._filename[:self._filename.rfind(".")] sdFilename = name[:8].lower() + ".gco" try: size = os.stat(self._file).st_size with open(self._file, "r") as f: self._comm.startSdFileTransfer(sdFilename) for line in f: if ";" in line: line = line[0:line.find(";")] line = line.strip() if len(line) > 0: self._comm.sendCommand(line) time.sleep(0.001) # do not send too fast self._progressCallback(sdFilename, float(f.tell()) / float(size)) finally: self._comm.endSdFileTransfer(sdFilename) self._finishCallback(sdFilename) class StateMonitor(object): 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._sdUploadData = None self._currentZ = None self._progress = None self._changeEvent = threading.Event() self._lastUpdate = time.time() self._worker = threading.Thread(target=self._work) self._worker.daemon = True self._worker.start() def reset(self, state=None, jobData=None, progress=None, currentZ=None): self.setState(state) self.setJobData(jobData) self.setProgress(progress) self.setCurrentZ(currentZ) def addTemperature(self, temperature): self._addTemperatureCallback(temperature) self._changeEvent.set() def addLog(self, log): self._addLogCallback(log) self._changeEvent.set() def addMessage(self, message): self._addMessageCallback(message) self._changeEvent.set() def setCurrentZ(self, currentZ): self._currentZ = currentZ self._changeEvent.set() def setState(self, state): self._state = state self._changeEvent.set() def setJobData(self, jobData): self._jobData = jobData self._changeEvent.set() def setProgress(self, progress): self._progress = progress self._changeEvent.set() def _work(self): while True: self._changeEvent.wait() now = time.time() delta = now - self._lastUpdate additionalWaitTime = self._ratelimit - delta if additionalWaitTime > 0: time.sleep(additionalWaitTime) data = self.getCurrentData() self._updateCallback(data) self._lastUpdate = time.time() self._changeEvent.clear() def getCurrentData(self): return { "state": self._state, "job": self._jobData, "currentZ": self._currentZ, "progress": self._progress }