From 333c9ba20511880be1264a03d1c0a66ed03e1d62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Tue, 3 Mar 2015 17:01:33 +0100 Subject: [PATCH] Refactoring of "printer" modules - renamed methods from camelCase to snake_case - renamed callback mathods from comm module from camelCase to snake_case - extracted and documented public interface to be used by plugins - extracted callback interface to be implemented by subscribed callbacks to printer - moved standard implementation to custom package - moved time estimation classes to custom package --- src/octoprint/events.py | 2 +- src/octoprint/printer/__init__.py | 1194 +++++------------ src/octoprint/printer/estimation.py | 63 + src/octoprint/printer/standard.py | 924 +++++++++++++ src/octoprint/server/__init__.py | 5 +- src/octoprint/server/api/connection.py | 8 +- src/octoprint/server/api/files.py | 38 +- src/octoprint/server/api/job.py | 16 +- src/octoprint/server/api/printer.py | 50 +- src/octoprint/server/api/settings.py | 4 +- src/octoprint/server/util/sockjs.py | 22 +- src/octoprint/server/util/watchdog.py | 4 +- .../templates/overlays/dragndrop.jinja2 | 6 +- src/octoprint/util/comm.py | 79 +- src/octoprint/util/virtual.py | 6 + tests/printer/test_estimation.py | 3 +- 16 files changed, 1447 insertions(+), 977 deletions(-) create mode 100644 src/octoprint/printer/estimation.py create mode 100644 src/octoprint/printer/standard.py diff --git a/src/octoprint/events.py b/src/octoprint/events.py index f16c23f8..70b2dce9 100644 --- a/src/octoprint/events.py +++ b/src/octoprint/events.py @@ -340,7 +340,7 @@ class CommandTrigger(GenericEventListener): "__now": datetime.datetime.now().isoformat() } - currentData = self._printer.getCurrentData() + currentData = self._printer.get_current_data() if "currentZ" in currentData.keys() and currentData["currentZ"] is not None: params["__currentZ"] = str(currentData["currentZ"]) diff --git a/src/octoprint/printer/__init__.py b/src/octoprint/printer/__init__.py index 3ab01b17..ea86749c 100644 --- a/src/octoprint/printer/__init__.py +++ b/src/octoprint/printer/__init__.py @@ -1,26 +1,42 @@ # coding=utf-8 +""" +This module defines the interface for communicating with a connected printer. + +The communication is in fact divided in two components, the :class:`PrinterInterface` and a deeper lying +communcation layer. However, plugins should only ever need to use the :class:`PrinterInterface` as the +abstracted version of the actual printer communiciation. + +.. autofunction:: get_connection_options + +.. autoclass:: PrinterInterface + :members: +""" + +from __future__ import absolute_import + __author__ = "Gina Häußge " __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' +__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License" -import time -import threading -import copy -import os -import logging +import re import octoprint.util.comm as comm -import octoprint.util as util - from octoprint.settings import settings -from octoprint.events import eventManager, Events -from octoprint.filemanager.destinations import FileDestinations - -from octoprint.plugin import plugin_manager, ProgressPlugin - -def getConnectionOptions(): +def get_connection_options(): """ - Retrieves the available ports, baudrates, prefered port and baudrate for connecting to the printer. + Retrieves the available ports, baudrates, prefered port and baudrate for connecting to the printer. + + Returned ``dict`` has the following structure:: + + ports: + baudrates: + portPreference: + baudratePreference: + autoconnect: + + Returns: + (dict): A dictionary holding the connection options in the structure specified above """ return { "ports": comm.serialList(), @@ -30,939 +46,393 @@ def getConnectionOptions(): "autoconnect": settings().getBoolean(["serial", "autoconnect"]) } -class Printer(comm.MachineComPrintCallback): - def __init__(self, fileManager, analysisQueue, printerProfileManager): - from collections import deque - self._logger = logging.getLogger(__name__) - #self._estimationLogger = logging.getLogger("ESTIMATIONS") - #self._printTimeLogger = logging.getLogger("PRINT_TIME") +class PrinterInterface(object): + """ + The :class:`PrinterInterface` represents the developer interface to the :class:`~octoprint.printer.standard.Printer` + instance. + """ - self._analysisQueue = analysisQueue - self._fileManager = fileManager - self._printerProfileManager = printerProfileManager + valid_axes = ("x", "y", "z", "e") + """Valid axes identifiers.""" - # state - # TODO do we really need to hold the temperature here? - self._temp = None - self._bedTemp = None - self._targetTemp = None - self._targetBedTemp = None - self._temps = 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._sdFilelistAvailable = threading.Event() - self._streamingFinishedCallback = None - - self._selectedFile = None - self._timeEstimationData = None - - # comm - self._comm = None - - # callbacks - self._callbacks = [] - - # progress plugins - self._lastProgressReport = None - self._progressPlugins = plugin_manager().get_implementations(ProgressPlugin) - - self._stateMonitor = StateMonitor( - ratelimit=0.5, - updateCallback=self._sendCurrentDataCallbacks, - addTemperatureCallback=self._sendAddTemperatureCallbacks, - addLogCallback=self._sendAddLogCallbacks, - addMessageCallback=self._sendAddMessageCallbacks - ) - self._stateMonitor.reset( - state={"text": self.getStateString(), "flags": self._getStateFlags()}, - jobData={ - "file": { - "name": None, - "size": None, - "origin": None, - "date": None - }, - "estimatedPrintTime": None, - "lastPrintTime": None, - "filament": { - "length": None, - "volume": None - } - }, - progress={"completion": None, "filepos": None, "printTime": None, "printTimeLeft": None}, - currentZ=None - ) - - eventManager().subscribe(Events.METADATA_ANALYSIS_FINISHED, self.onMetadataAnalysisFinished) - eventManager().subscribe(Events.METADATA_STATISTICS_UPDATED, self.onMetadataStatisticsUpdated) - - #~~ 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: self._logger.exception("Exception while adding temperature data point") - - def _sendAddLogCallbacks(self, data): - for callback in self._callbacks: - try: callback.addLog(data) - except: self._logger.exception("Exception while adding communication log entry") - - def _sendAddMessageCallbacks(self, data): - for callback in self._callbacks: - try: callback.addMessage(data) - except: self._logger.exception("Exception while adding printer message") - - def _sendCurrentDataCallbacks(self, data): - for callback in self._callbacks: - try: callback.sendCurrentData(copy.deepcopy(data)) - except: self._logger.exception("Exception while pushing current data") - - def _sendTriggerUpdateCallbacks(self, type): - for callback in self._callbacks: - try: callback.sendEvent(type) - except: self._logger.exception("Exception while pushing trigger update") - - def _sendFeedbackCommandOutput(self, name, output): - for callback in self._callbacks: - try: callback.sendFeedbackCommandOutput(name, output) - except: self._logger.exception("Exception while pushing feedback command output") - - #~~ callback from metadata analysis event - - def onMetadataAnalysisFinished(self, event, data): - if self._selectedFile: - self._setJobData(self._selectedFile["filename"], - self._selectedFile["filesize"], - self._selectedFile["sd"]) - - def onMetadataStatisticsUpdated(self, event, data): - self._setJobData(self._selectedFile["filename"], - self._selectedFile["filesize"], - self._selectedFile["sd"]) - - #~~ progress plugin reporting - - def _reportPrintProgressToPlugins(self, progress): - if not progress or not self._selectedFile or not "sd" in self._selectedFile or not "filename" in self._selectedFile: - return - - storage = "sdcard" if self._selectedFile["sd"] else "local" - filename = self._selectedFile["filename"] - - def call_plugins(storage, filename, progress): - for name, plugin in self._progressPlugins.items(): - try: - plugin.on_print_progress(storage, filename, progress) - except: - self._logger.exception("Exception while sending print progress to plugin %s" % name) - - thread = threading.Thread(target=call_plugins, args=(storage, filename, progress)) - thread.daemon = False - thread.start() - - #~~ printer commands + valid_tool_regex = re.compile("^(tool[0-9]+|bed)$") + """Regex for valid tool identifiers.""" def connect(self, port=None, baudrate=None, profile=None): """ - Connects to the printer. If port and/or baudrate is provided, uses these settings, otherwise autodetection - will be attempted. + Connects to the printer, using the specified serial ``port``, ``baudrate`` and printer ``profile``. If a + connection is already established, that connection will be closed prior to connecting anew with the provided + parameters. + + Arguments: + port (string): Name of the serial port to connect to. If not provided, an auto detection will be attempted. + baudrate (int): Baudrate to connect with. If not provided, an auto detection will be attempted. + profile (string): Name of the printer profile to use for this connection. If not provided, the default + will be retrieved from the :class:`PrinterProfileManager`. """ - if self._comm is not None: - self._comm.close() - self._printerProfileManager.select(profile) - self._comm = comm.MachineCom(port, baudrate, callbackObject=self, printerProfileManager=self._printerProfileManager) + pass def disconnect(self): """ - Closes the connection to the printer. + Disconnects from the printer. Does nothing if no connection is currently established. """ - if self._comm is not None: - self._comm.close() - self._comm = None - self._printerProfileManager.deselect() - eventManager().fire(Events.DISCONNECTED) + raise NotImplementedError() - def getTransport(self): + def get_transport(self): """ Returns the communication layer's transport object, if a connection is currently established. Note that this doesn't have to necessarily be a :class:`serial.Serial` instance, it might also be something different, so take care to do instance checks before attempting to access any properties or methods. - :return: the communication layer's transport object + Returns: + object: The communication layer's transport object """ - - if self._comm is None: - return None - - return self._comm.getTransport() - - def command(self, command): - """ - Sends a single gcode command to the printer. - """ - self.commands([command]) + raise NotImplementedError() def commands(self, commands): """ - Sends multiple gcode commands (provided as a list) to the printer. - """ - if self._comm is None: - return + Sends the provided ``commands`` to the printer. - for command in commands: - self._comm.sendCommand(command) + Arguments: + commands (string, list): The commands to send. Might be a single command provided just as a string or a list + of multiple commands to send in order. + """ + raise NotImplementedError() def jog(self, axis, amount): - printer_profile = self._printerProfileManager.get_current_or_default() - movement_speed = printer_profile["axes"][axis]["speed"] - self.commands(["G91", "G1 %s%.4f F%d" % (axis.upper(), amount, movement_speed), "G90"]) + """ + Jogs the specified printer ``axis`` by the specified ``amount`` in mm. + + Arguments: + axis (string): The axis to jog, will be converted to lower case, one of "x", "y", "z" or "e" + amount (int, float): The amount by which to jog in mm + """ + raise NotImplementedError() def home(self, axes): - self.commands(["G91", "G28 %s" % " ".join(map(lambda x: "%s0" % x.upper(), axes)), "G90"]) + """ + Homes the specified printer ``axes``. + + Arguments: + axes (string, list): The axis or axes to home, each of which must converted to lower case must match one of + "x", "y", "z" and "e" + """ + raise NotImplementedError() def extrude(self, amount): - printer_profile = self._printerProfileManager.get_current_or_default() - extrusion_speed = printer_profile["axes"]["e"]["speed"] - self.commands(["G91", "G1 E%s F%d" % (amount, extrusion_speed), "G90"]) - - def changeTool(self, tool): - try: - toolNum = int(tool[len("tool"):]) - self.command("T%d" % toolNum) - except ValueError: - pass - - def setTemperature(self, type, value): - if type.startswith("tool"): - printer_profile = self._printerProfileManager.get_current_or_default() - extruder_count = printer_profile["extruder"]["count"] - if extruder_count > 1: - try: - toolNum = int(type[len("tool"):]) - self.command("M104 T%d S%f" % (toolNum, value)) - except ValueError: - pass - else: - self.command("M104 S%f" % value) - elif type == "bed": - self.command("M140 S%f" % value) - - def setTemperatureOffset(self, offsets={}): - if self._comm is None: - return - - tool, bed = self._comm.getOffsets() - - validatedOffsets = {} - - for key in offsets: - value = offsets[key] - if key == "bed": - bed = value - validatedOffsets[key] = value - elif key.startswith("tool"): - try: - toolNum = int(key[len("tool"):]) - tool[toolNum] = value - validatedOffsets[key] = value - except ValueError: - pass - - self._comm.setTemperatureOffset(tool, bed) - self._stateMonitor.setTempOffsets(validatedOffsets) - - def _convertRateValue(self, factor, min=0, max=200): - if not isinstance(factor, (int, float, long)): - raise ValueError("factor is not a number") - - if isinstance(factor, float): - factor = int(factor * 100.0) - - if factor < min or factor > max: - raise ValueError("factor must be a value between %f and %f" % (min, max)) - - return factor - - def feedRate(self, factor): - factor = self._convertRateValue(factor, min=50, max=200) - self.command("M220 S%d" % factor) - - def flowRate(self, factor): - factor = self._convertRateValue(factor, min=75, max=125) - self.command("M221 S%d" % factor) - - def selectFile(self, filename, sd, printAfterSelect=False): - if self._comm is None or (self._comm.isBusy() or self._comm.isStreaming()): - self._logger.info("Cannot load file: printer not connected or currently busy") - return - - self._printAfterSelect = printAfterSelect - self._comm.selectFile("/" + filename if sd else filename, sd) - self._setProgressData(0, None, None, None) - self._setCurrentZ(None) - - def unselectFile(self): - if self._comm is not None and (self._comm.isBusy() or self._comm.isStreaming()): - return - - self._comm.unselectFile() - self._setProgressData(0, None, None, None) - self._setCurrentZ(None) - - 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 + Extrude ``amount`` milimeters of material from the tool. + + Arguments: + amount (int, float): The amount of material to extrude in mm """ - if self._comm is None or not self._comm.isOperational() or self._comm.isPrinting(): - return - if self._selectedFile is None: - return + raise NotImplementedError() - self._timeEstimationData = TimeEstimationHelper() - self._lastProgressReport = None - self._setCurrentZ(None) - self._comm.startPrint() - - def togglePausePrint(self): + def change_tool(self, tool): """ - Pause the current printjob. + Switch the currently active ``tool`` (for which extrude commands will apply). + + Arguments: + tool (int): The tool to switch to, index starting at 0, so first tool is 0, second 1, ... """ - if self._comm is None: - return + raise NotImplementedError() - self._comm.setPause(not self._comm.isPaused()) - - def cancelPrint(self): + def set_temperature(self, heater, value): """ - Cancel the current printjob. + Sets the target temperature on the specified ``heater`` to the given ``value`` in celsius. + + Arguments: + heater (string): The heater for which to set the target temperature. Either "bed" for setting the bed + temperature or something matching the regular expression "tool[0-9]+" (e.g. "tool0", "tool1", ...) for + the hotends of the printer + value (int, float): The temperature in celsius to set the target temperature to. """ - if self._comm is None: - return + raise NotImplementedError() - self._comm.cancelPrint() - - # 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._fileManager.log_print(FileDestinations.SDCARD if self._selectedFile["sd"] else FileDestinations.LOCAL, self._selectedFile["filename"], time.time(), self._comm.getPrintTime(), False, self._printerProfileManager.get_current_or_default()["id"]) - payload = { - "file": self._selectedFile["filename"], - "origin": FileDestinations.LOCAL - } - if self._selectedFile["sd"]: - payload["origin"] = FileDestinations.SDCARD - eventManager().fire(Events.PRINT_FAILED, payload) - - #~~ state monitoring - - def _setCurrentZ(self, currentZ): - self._currentZ = currentZ - self._stateMonitor.setCurrentZ(self._currentZ) - - def _setState(self, state): - self._state = state - self._stateMonitor.setState({"text": 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 _estimateTotalPrintTime(self, progress, printTime): - if not progress or not printTime or not self._timeEstimationData: - #self._estimationLogger.info("{progress};{printTime};;;;".format(**locals())) - return None - - else: - newEstimate = printTime / progress - self._timeEstimationData.update(newEstimate) - - result = None - if self._timeEstimationData.is_stable(): - result = self._timeEstimationData.average_total_rolling - - #averageTotal = self._timeEstimationData.average_total - #averageTotalRolling = self._timeEstimationData.average_total_rolling - #averageDistance = self._timeEstimationData.average_distance - - #self._estimationLogger.info("{progress};{printTime};{newEstimate};{averageTotal};{averageTotalRolling};{averageDistance}".format(**locals())) - - return result - - def _setProgressData(self, progress, filepos, printTime, cleanedPrintTime): - estimatedTotalPrintTime = self._estimateTotalPrintTime(progress, cleanedPrintTime) - statisticalTotalPrintTime = None - totalPrintTime = estimatedTotalPrintTime - - if self._selectedFile and "estimatedPrintTime" in self._selectedFile and self._selectedFile["estimatedPrintTime"]: - statisticalTotalPrintTime = self._selectedFile["estimatedPrintTime"] - if progress and cleanedPrintTime: - if estimatedTotalPrintTime is None: - totalPrintTime = statisticalTotalPrintTime - else: - if progress < 0.5: - sub_progress = progress * 2 - else: - sub_progress = 1.0 - totalPrintTime = (1 - sub_progress) * statisticalTotalPrintTime + sub_progress * estimatedTotalPrintTime - - #self._printTimeLogger.info("{progress};{cleanedPrintTime};{estimatedTotalPrintTime};{statisticalTotalPrintTime};{totalPrintTime}".format(**locals())) - - self._progress = progress - self._printTime = printTime - self._printTimeLeft = totalPrintTime - cleanedPrintTime if (totalPrintTime is not None and cleanedPrintTime is not None) else None - - self._stateMonitor.setProgress({ - "completion": self._progress * 100 if self._progress is not None else None, - "filepos": filepos, - "printTime": int(self._printTime) if self._printTime is not None else None, - "printTimeLeft": int(self._printTimeLeft) if self._printTimeLeft is not None else None - }) - - if progress: - progress_int = int(progress * 100) - if self._lastProgressReport != progress_int: - self._lastProgressReport = progress_int - self._reportPrintProgressToPlugins(progress_int) - - - def _addTemperatureData(self, temp, bedTemp): - currentTimeUtc = int(time.time()) - - data = { - "time": currentTimeUtc - } - for tool in temp.keys(): - data["tool%d" % tool] = { - "actual": temp[tool][0], - "target": temp[tool][1] - } - if bedTemp is not None and isinstance(bedTemp, tuple): - data["bed"] = { - "actual": bedTemp[0], - "target": bedTemp[1] - } - - self._temps.append(data) - - self._temp = temp - self._bedTemp = bedTemp - - self._stateMonitor.addTemperature(data) - - def _setJobData(self, filename, filesize, sd): - if filename is not None: - if sd: - path_in_storage = filename[1:] - path_on_disk = None - else: - path_in_storage = self._fileManager.path_in_storage(FileDestinations.LOCAL, filename) - path_on_disk = self._fileManager.path_on_disk(FileDestinations.LOCAL, filename) - self._selectedFile = { - "filename": path_in_storage, - "filesize": filesize, - "sd": sd, - "estimatedPrintTime": None - } - else: - self._selectedFile = None - self._stateMonitor.setJobData({ - "file": { - "name": None, - "origin": None, - "size": None, - "date": None - }, - "estimatedPrintTime": None, - "averagePrintTime": None, - "lastPrintTime": None, - "filament": None, - }) - return - - estimatedPrintTime = None - lastPrintTime = None - averagePrintTime = None - date = None - filament = None - if path_on_disk: - # Use a string for mtime because it could be float and the - # javascript needs to exact match - if not sd: - date = int(os.stat(path_on_disk).st_ctime) - - try: - fileData = self._fileManager.get_metadata(FileDestinations.SDCARD if sd else FileDestinations.LOCAL, path_on_disk) - except: - fileData = None - if fileData is not None: - if "analysis" in fileData: - if estimatedPrintTime is None and "estimatedPrintTime" in fileData["analysis"]: - estimatedPrintTime = fileData["analysis"]["estimatedPrintTime"] - if "filament" in fileData["analysis"].keys(): - filament = fileData["analysis"]["filament"] - if "statistics" in fileData: - printer_profile = self._printerProfileManager.get_current_or_default()["id"] - if "averagePrintTime" in fileData["statistics"] and printer_profile in fileData["statistics"]["averagePrintTime"]: - averagePrintTime = fileData["statistics"]["averagePrintTime"][printer_profile] - if "lastPrintTime" in fileData["statistics"] and printer_profile in fileData["statistics"]["lastPrintTime"]: - lastPrintTime = fileData["statistics"]["lastPrintTime"][printer_profile] - - if averagePrintTime is not None: - self._selectedFile["estimatedPrintTime"] = averagePrintTime - elif estimatedPrintTime is not None: - # TODO apply factor which first needs to be tracked! - self._selectedFile["estimatedPrintTime"] = estimatedPrintTime - - self._stateMonitor.setJobData({ - "file": { - "name": path_in_storage, - "origin": FileDestinations.SDCARD if sd else FileDestinations.LOCAL, - "size": filesize, - "date": date - }, - "estimatedPrintTime": estimatedPrintTime, - "averagePrintTime": averagePrintTime, - "lastPrintTime": lastPrintTime, - "filament": filament, - }) - - def _sendInitialStateUpdate(self, callback): - try: - data = self._stateMonitor.getCurrentData() - data.update({ - "temps": list(self._temps), - "logs": list(self._log), - "messages": list(self._messages) - }) - callback.sendHistoryData(data) - 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(), - "paused": self.isPaused(), - "ready": self.isReady(), - "sdReady": self.isSdReady() - } - - #~~ callbacks triggered from self._comm - - def mcLog(self, message): + def set_temperature_offset(self, offsets=None): """ - Callback method for the comm object, called upon log output. + Sets the temperature ``offsets`` to apply to target temperatures red from a GCODE file while printing. + + Arguments: + offsets (dict): A dictionary specifying the offsets to apply. Keys must match the format for the ``heater`` + parameter to :func:`set_temperature`, so "bed" for the offset for the bed target temperature and + "tool[0-9]+" for the offsets to the hotend target temperatures. """ - self._addLog(message) + raise NotImplementedError() - def mcTempUpdate(self, temp, bedTemp): - self._addTemperatureData(temp, bedTemp) - - def mcStateChange(self, state): + def feed_rate(self, factor): """ - Callback method for the comm object, called if the connection state changes. + Sets the ``factor`` for the printer's feed rate. + + Arguments: + factor (int, float): The factor for the feed rate to send to the firmware. Percentage expressed as either an + int between 0 and 100 or a float between 0 and 1. """ - oldState = self._state + raise NotImplementedError() - # 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._fileManager.log_print(FileDestinations.SDCARD if self._selectedFile["sd"] else FileDestinations.LOCAL, self._selectedFile["filename"], time.time(), self._comm.getPrintTime(), True, self._printerProfileManager.get_current_or_default()["id"]) - elif state == self._comm.STATE_CLOSED or state == self._comm.STATE_ERROR or state == self._comm.STATE_CLOSED_WITH_ERROR: - self._fileManager.log_print(FileDestinations.SDCARD if self._selectedFile["sd"] else FileDestinations.LOCAL, self._selectedFile["filename"], time.time(), self._comm.getPrintTime(), False, self._printerProfileManager.get_current_or_default()["id"]) - self._analysisQueue.resume() # printing done, put those cpu cycles to good use - elif self._comm is not None and state == self._comm.STATE_PRINTING: - self._analysisQueue.pause() # do not analyse files while printing - - self._setState(state) - - def mcMessage(self, message): + def flow_rate(self, factor): """ - 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. + Sets the ``factor`` for the printer's flow rate. + + Arguments: + factor (int, float): The factor for the flow rate to send to the firmware. Percentage expressed as either an + int between 0 and 100 or a float between 0 and 1. """ - self._addMessage(message) + raise NotImplementedError() - def mcProgress(self): + def select_file(self, path, sd, printAfterSelect=False): """ - 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. + Selects the specified ``path`` for printing, specifying if the file is to be found on the ``sd`` or not. + Optionally can also directly start the print after selecting the file. + + Arguments: + path (string): The path to select for printing. Either an absolute path (local file) or a """ + raise NotImplementedError() - self._setProgressData(self._comm.getPrintProgress(), self._comm.getPrintFilepos(), self._comm.getPrintTime(), self._comm.getCleanedPrintTime()) - - def mcZChange(self, newZ): + def unselect_file(self): """ - Callback method for the comm object, called upon change of the z-layer. + Unselects and currently selected file. """ - 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(Events.Z_CHANGE, {"new": newZ, "old": oldZ}) + raise NotImplementedError() - self._setCurrentZ(newZ) - - def mcSdStateChange(self, sdReady): - self._stateMonitor.setState({"text": self.getStateString(), "flags": self._getStateFlags()}) - - def mcSdFiles(self, files): - eventManager().fire(Events.UPDATED_FILES, {"type": "gcode"}) - self._sdFilelistAvailable.set() - - def mcFileSelected(self, filename, filesize, sd): - self._setJobData(filename, filesize, sd) - self._stateMonitor.setState({"text": 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({"text": self.getStateString(), "flags": self._getStateFlags()}) - - def mcFileTransferStarted(self, filename, filesize): - self._sdStreaming = True - - self._setJobData(filename, filesize, True) - self._setProgressData(0.0, 0, 0, None) - self._stateMonitor.setState({"text": self.getStateString(), "flags": self._getStateFlags()}) - - def mcFileTransferDone(self, filename): - self._sdStreaming = False - - if self._streamingFinishedCallback is not None: - # in case of SD files, both filename and absolutePath are the same, so we set the (remote) filename for - # both parameters - self._streamingFinishedCallback(filename, filename, FileDestinations.SDCARD) - - self._setCurrentZ(None) - self._setJobData(None, None, None) - self._setProgressData(None, None, None, None) - self._stateMonitor.setState({"text": self.getStateString(), "flags": self._getStateFlags()}) - - def mcReceivedRegisteredMessage(self, command, output): - self._sendFeedbackCommandOutput(command, output) - - def mcForceDisconnect(self): - self.disconnect() - - #~~ sd file handling - - def getSdFiles(self): - if self._comm is None or not self._comm.isSdReady(): - return [] - return map(lambda x: (x[0][1:], x[1]), self._comm.getSdFiles()) - - def addSdFile(self, filename, absolutePath, streamingFinishedCallback): - if not self._comm or self._comm.isBusy() or not self._comm.isSdReady(): - self._logger.error("No connection to printer or printer is busy") - return - - self._streamingFinishedCallback = streamingFinishedCallback - - self.refreshSdFiles(blocking=True) - existingSdFiles = map(lambda x: x[0], self._comm.getSdFiles()) - - remoteName = util.get_dos_filename(filename, existing_filenames=existingSdFiles, extension="gco") - self._timeEstimationData = TimeEstimationHelper() - self._comm.startFileTransfer(absolutePath, filename, "/" + remoteName) - - return remoteName - - def deleteSdFile(self, filename): - if not self._comm or not self._comm.isSdReady(): - return - self._comm.deleteSdFile("/" + filename) - - def initSdCard(self): - if not self._comm or self._comm.isSdReady(): - return - self._comm.initSdCard() - - def releaseSdCard(self): - if not self._comm or not self._comm.isSdReady(): - return - self._comm.releaseSdCard() - - def refreshSdFiles(self, blocking=False): + def start_print(self): """ - Refreshs the list of file stored on the SD card attached to printer (if available and printer communication - available). Optional blocking parameter allows making the method block (max 10s) until the file list has been - received (and can be accessed via self._comm.getSdFiles()). Defaults to an asynchronous operation. + Starts printing the currently selected file. If no file is currently selected, does nothing. """ - if not self._comm or not self._comm.isSdReady(): - return - self._sdFilelistAvailable.clear() - self._comm.refreshSdFiles() - if blocking: - self._sdFilelistAvailable.wait(10000) + raise NotImplementedError() - #~~ state reports - - def getStateString(self): + def toggle_pause_print(self): """ - Returns a human readable string corresponding to the current communication state. + Pauses the current print job if it is currently running or resumes it if it is currently paused. """ - if self._comm is None: - return "Offline" - else: - return self._comm.getStateString() + raise NotImplementedError() - def getCurrentData(self): - return self._stateMonitor.getCurrentData() + def cancel_print(self): + """ + Cancels the current print job. + """ + raise NotImplementedError() - def getCurrentJob(self): - currentData = self._stateMonitor.getCurrentData() - return currentData["job"] + def get_state_string(self): + """ + Returns: + (string) A human readable string corresponding to the current communication state. + """ + raise NotImplementedError() - def getCurrentTemperatures(self): - if self._comm is not None: - tempOffset, bedTempOffset = self._comm.getOffsets() - else: - tempOffset = {} - bedTempOffset = None + def get_current_data(self): + """ + Returns: + (dict) The current state data. + """ + raise NotImplementedError() - result = {} - if self._temp is not None: - for tool in self._temp.keys(): - result["tool%d" % tool] = { - "actual": self._temp[tool][0], - "target": self._temp[tool][1], - "offset": tempOffset[tool] if tool in tempOffset.keys() and tempOffset[tool] is not None else 0 - } - if self._bedTemp is not None: - result["bed"] = { - "actual": self._bedTemp[0], - "target": self._bedTemp[1], - "offset": bedTempOffset - } + def get_current_job(self): + """ + Returns: + (dict) The data of the current job. + """ + raise NotImplementedError() - return result + def get_current_temperatures(self): + """ + Returns: + (dict) The current temperatures. + """ + raise NotImplementedError() - def getTemperatureHistory(self): - return self._temps + def get_temperature_history(self): + """ + Returns: + (list) The temperature history. + """ + raise NotImplementedError() - def getCurrentConnection(self): - if self._comm is None: - return "Closed", None, None, None + def get_current_connection(self): + """ + Returns: + (tuple) The current connection information as a 4-tuple ``(connection_string, port, baudrate, printer_profile)``. + If the printer is currently not connected, the tuple will be ``("Closed", None, None, None)``. + """ + raise NotImplementedError() - port, baudrate = self._comm.getConnection() - printer_profile = self._printerProfileManager.get_current_or_default() - return self._comm.getStateString(), port, baudrate, printer_profile + def is_closed_or_error(self): + """ + Returns: + (boolean) Whether the printer is currently disconnected and/or in an error state. + """ + raise NotImplementedError() - def isClosedOrError(self): - return self._comm is None or self._comm.isClosedOrError() + def is_operational(self): + """ + Returns: + (boolean) Whether the printer is currently connected and available. + """ + raise NotImplementedError() - def isOperational(self): - return self._comm is not None and self._comm.isOperational() + def is_printing(self): + """ + Returns: + (boolean) Whether the printer is currently printing. + """ + raise NotImplementedError() - def isPrinting(self): - return self._comm is not None and self._comm.isPrinting() + def is_paused(self): + """ + Returns: + (boolean) Whether the printer is currently paused. + """ + raise NotImplementedError() - def isPaused(self): - return self._comm is not None and self._comm.isPaused() + def is_error(self): + """ + Returns: + (boolean) Whether the printer is currently in an error state. + """ + raise NotImplementedError() - def isError(self): - return self._comm is not None and self._comm.isError() + def is_ready(self): + """ + Returns: + (boolean) Whether the printer is currently operational and ready for new print jobs (not printing). + """ + raise NotImplementedError() - def isReady(self): - return self.isOperational() and not self._comm.isStreaming() + def register_callback(self, callback): + """ + Registers a :class:`PrinterCallback` with the instance. - def isSdReady(self): - if not settings().getBoolean(["feature", "sdSupport"]) or self._comm is None: - return False - else: - return self._comm.isSdReady() + Arguments: + callback (PrinterCallback): The callback object to register. + """ + raise NotImplementedError() -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 + def unregister_callback(self, callback): + """ + Unregisters a :class:`PrinterCallback` from the instance. - self._state = None - self._jobData = None - self._gcodeData = None - self._sdUploadData = None - self._currentZ = None - self._progress = None - - self._offsets = {} - - self._changeEvent = threading.Event() - self._stateMutex = threading.Lock() - - 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): - with self._stateMutex: - 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 setTempOffsets(self, offsets): - self._offsets = offsets - self._changeEvent.set() - - def _work(self): - while True: - self._changeEvent.wait() - - with self._stateMutex: - 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, - "offsets": self._offsets - } + Arguments: + callback (PrinterCallback): The callback object to unregister. + """ + raise NotImplementedError() -class TimeEstimationHelper(object): +class PrinterCallback(object): + def on_printer_add_log(self, data): + """ + Called when the :class:`PrinterInterface` receives a new communication log entry from the communication layer. - STABLE_THRESHOLD = 0.1 - STABLE_COUNTDOWN = 250 - STABLE_ROLLING_WINDOW = 250 + Arguments: + data (string): The received log line. + """ + pass - def __init__(self): - import collections - self._distances = collections.deque([], self.__class__.STABLE_ROLLING_WINDOW) - self._totals = collections.deque([], self.__class__.STABLE_ROLLING_WINDOW) - self._sum_total = 0 - self._count = 0 - self._stable_counter = None + def on_printer_add_message(self, data): + """ + Called when the :class:`PrinterInterface` receives a new message from the communication layer. - def is_stable(self): - return self._stable_counter is not None and self._stable_counter >= self.__class__.STABLE_COUNTDOWN + Arguments: + data (string): The received message. + """ + pass - def update(self, newEstimate): - old_average_total = self.average_total + def on_printer_add_temperature(self, data): + """ + Called when the :class:`PrinterInterface` receives a new temperature data set from the communication layer. - self._sum_total += newEstimate - self._totals.append(newEstimate) - self._count += 1 + ``data`` is a ``dict`` of the following structure:: - if old_average_total: - self._distances.append(abs(self.average_total - old_average_total)) + tool0: + actual: + target: + ... + bed: + actual: + target: - if -1.0 * self.__class__.STABLE_THRESHOLD < self.average_distance < self.__class__.STABLE_THRESHOLD: - if self._stable_counter is None: - self._stable_counter = 0 - else: - self._stable_counter += 1 - else: - self._stable_counter = None + Arguments: + data (dict): A dict of all current temperatures in the format as specified above + """ + pass - @property - def average_total(self): - if not self._count: - return None - else: - return self._sum_total / self._count + def on_printer_received_registered_message(self, name, output): + """ + Called when the :class:`PrinterInterface` received a registered message, e.g. from a feedback command. - @property - def average_total_rolling(self): - if not self._count or self._count < self.__class__.STABLE_ROLLING_WINDOW: - return None - else: - return sum(self._totals) / len(self._totals) + Arguments: + name (string): Name of the registered message (e.g. the feedback command) + output (string): Output for the registered message + """ + pass + + def on_printer_send_initial_data(self, data): + """ + Called when registering as a callback with the :class:`PrinterInterface` to receive the initial data (state, + log and temperature history etc) from the printer. + + ``data`` is a ``dict`` of the following structure:: + + temps: + - time: + tool0: + actual: + target: + ... + bed: + actual: + target: + - ... + logs: + messages: + + Arguments: + data (dict): The initial data in the format as specified above. + """ + pass + + def on_printer_send_current_data(self, data): + """ + Called when the internal state of the :class:`PrinterInterface` changes, due to changes in the printer state, + temperatures, log lines, job progress etc. Updates via this method are guaranteed to be throttled to a maximum + of 2 calles per second. + + ``data`` is a ``dict`` of the following structure:: + + state: + text: + flags: + operational: + printing: + closedOrError: + error: + paused: + ready: + sdReady: + job: + file: + name: , + size: , + origin: , + date: + estimatedPrintTime: + lastPrintTime: + filament: + length: + volume: + progress: + completion: + filepos: + printTime: + printTimeLeft: + currentZ: + offsets: + + Arguments: + data (dict): The current data in the format as specified above. + """ + pass - @property - def average_distance(self): - if not self._count or self._count < self.__class__.STABLE_ROLLING_WINDOW + 1: - return None - else: - return sum(self._distances) / len(self._distances) diff --git a/src/octoprint/printer/estimation.py b/src/octoprint/printer/estimation.py new file mode 100644 index 00000000..317344d7 --- /dev/null +++ b/src/octoprint/printer/estimation.py @@ -0,0 +1,63 @@ +# coding=utf-8 +from __future__ import absolute_import + +__author__ = "Gina Häußge " +__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' +__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License" + + +class TimeEstimationHelper(object): + + STABLE_THRESHOLD = 0.1 + STABLE_COUNTDOWN = 250 + STABLE_ROLLING_WINDOW = 250 + + def __init__(self): + import collections + self._distances = collections.deque([], self.__class__.STABLE_ROLLING_WINDOW) + self._totals = collections.deque([], self.__class__.STABLE_ROLLING_WINDOW) + self._sum_total = 0 + self._count = 0 + self._stable_counter = None + + def is_stable(self): + return self._stable_counter is not None and self._stable_counter >= self.__class__.STABLE_COUNTDOWN + + def update(self, newEstimate): + old_average_total = self.average_total + + self._sum_total += newEstimate + self._totals.append(newEstimate) + self._count += 1 + + if old_average_total: + self._distances.append(abs(self.average_total - old_average_total)) + + if -1.0 * self.__class__.STABLE_THRESHOLD < self.average_distance < self.__class__.STABLE_THRESHOLD: + if self._stable_counter is None: + self._stable_counter = 0 + else: + self._stable_counter += 1 + else: + self._stable_counter = None + + @property + def average_total(self): + if not self._count: + return None + else: + return self._sum_total / self._count + + @property + def average_total_rolling(self): + if not self._count or self._count < self.__class__.STABLE_ROLLING_WINDOW: + return None + else: + return sum(self._totals) / len(self._totals) + + @property + def average_distance(self): + if not self._count or self._count < self.__class__.STABLE_ROLLING_WINDOW + 1: + return None + else: + return sum(self._distances) / len(self._distances) \ No newline at end of file diff --git a/src/octoprint/printer/standard.py b/src/octoprint/printer/standard.py new file mode 100644 index 00000000..6698931c --- /dev/null +++ b/src/octoprint/printer/standard.py @@ -0,0 +1,924 @@ +# coding=utf-8 +""" +This module holds the standard implementation of the :class:`PrinterInterface` and it helpers. +""" + +from __future__ import absolute_import + +__author__ = "Gina Häußge " +__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' +__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License" + +import copy +import logging +import os +import threading +import time + +from octoprint import util as util +from octoprint.events import eventManager, Events +from octoprint.filemanager import FileDestinations +from octoprint.plugin import plugin_manager, ProgressPlugin +from octoprint.printer import PrinterInterface, PrinterCallback +from octoprint.printer.estimation import TimeEstimationHelper +from octoprint.settings import settings +from octoprint.util import comm as comm + + +class Printer(PrinterInterface, comm.MachineComPrintCallback): + """ + Default implementation of the :class:`PrinterInterface`. Manages the communication layer object and registers + itself with it as a callback to react to changes on the communication layer. + """ + + def __init__(self, fileManager, analysisQueue, printerProfileManager): + from collections import deque + + self._logger = logging.getLogger(__name__) + + self._analysisQueue = analysisQueue + self._fileManager = fileManager + self._printerProfileManager = printerProfileManager + + # state + # TODO do we really need to hold the temperature here? + self._temp = None + self._bedTemp = None + self._targetTemp = None + self._targetBedTemp = None + self._temps = 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._sdFilelistAvailable = threading.Event() + self._streamingFinishedCallback = None + + self._selectedFile = None + self._timeEstimationData = None + + # comm + self._comm = None + + # callbacks + self._callbacks = [] + + # progress plugins + self._lastProgressReport = None + self._progressPlugins = plugin_manager().get_implementations(ProgressPlugin) + + self._stateMonitor = StateMonitor( + interval=0.5, + on_update=self._sendCurrentDataCallbacks, + on_add_temperature=self._sendAddTemperatureCallbacks, + on_add_log=self._sendAddLogCallbacks, + on_add_message=self._sendAddMessageCallbacks + ) + self._stateMonitor.reset( + state={"text": self.get_state_string(), "flags": self._getStateFlags()}, + job_data={ + "file": { + "name": None, + "size": None, + "origin": None, + "date": None + }, + "estimatedPrintTime": None, + "lastPrintTime": None, + "filament": { + "length": None, + "volume": None + } + }, + progress={"completion": None, "filepos": None, "printTime": None, "printTimeLeft": None}, + current_z=None + ) + + eventManager().subscribe(Events.METADATA_ANALYSIS_FINISHED, self._on_event_MetadataAnalysisFinished) + eventManager().subscribe(Events.METADATA_STATISTICS_UPDATED, self._on_event_MetadataStatisticsUpdated) + + #~~ handling of PrinterCallbacks + + def register_callback(self, callback): + if not isinstance(callback, PrinterCallback): + self._logger.warn("Registering an object as printer callback which doesn't implement the PrinterCallback interface") + + self._callbacks.append(callback) + self._sendInitialStateUpdate(callback) + + def unregister_callback(self, callback): + if callback in self._callbacks: + self._callbacks.remove(callback) + + def _sendAddTemperatureCallbacks(self, data): + for callback in self._callbacks: + try: callback.on_printer_add_temperature(data) + except: self._logger.exception("Exception while adding temperature data point") + + def _sendAddLogCallbacks(self, data): + for callback in self._callbacks: + try: callback.on_printer_add_log(data) + except: self._logger.exception("Exception while adding communication log entry") + + def _sendAddMessageCallbacks(self, data): + for callback in self._callbacks: + try: callback.on_printer_add_message(data) + except: self._logger.exception("Exception while adding printer message") + + def _sendCurrentDataCallbacks(self, data): + for callback in self._callbacks: + try: callback.on_printer_send_current_data(copy.deepcopy(data)) + except: self._logger.exception("Exception while pushing current data") + + def _sendFeedbackCommandOutput(self, name, output): + for callback in self._callbacks: + try: callback.on_printer_received_registered_message(name, output) + except: self._logger.exception("Exception while pushing feedback command output") + + #~~ callback from metadata analysis event + + def _on_event_MetadataAnalysisFinished(self, event, data): + if self._selectedFile: + self._setJobData(self._selectedFile["filename"], + self._selectedFile["filesize"], + self._selectedFile["sd"]) + + def _on_event_MetadataStatisticsUpdated(self, event, data): + self._setJobData(self._selectedFile["filename"], + self._selectedFile["filesize"], + self._selectedFile["sd"]) + + #~~ progress plugin reporting + + def _reportPrintProgressToPlugins(self, progress): + if not progress or not self._selectedFile or not "sd" in self._selectedFile or not "filename" in self._selectedFile: + return + + storage = "sdcard" if self._selectedFile["sd"] else "local" + filename = self._selectedFile["filename"] + + def call_plugins(storage, filename, progress): + for name, plugin in self._progressPlugins.items(): + try: + plugin.on_print_progress(storage, filename, progress) + except: + self._logger.exception("Exception while sending print progress to plugin %s" % name) + + thread = threading.Thread(target=call_plugins, args=(storage, filename, progress)) + thread.daemon = False + thread.start() + + #~~ PrinterInterface implementation + + def connect(self, port=None, baudrate=None, profile=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._printerProfileManager.select(profile) + self._comm = comm.MachineCom(port, baudrate, callbackObject=self, printerProfileManager=self._printerProfileManager) + + def disconnect(self): + """ + Closes the connection to the printer. + """ + if self._comm is not None: + self._comm.close() + self._comm = None + self._printerProfileManager.deselect() + eventManager().fire(Events.DISCONNECTED) + + def get_transport(self): + + if self._comm is None: + return None + + return self._comm.getTransport() + getTransport = util.deprecated("getTransport has been renamed to get_transport", since="1.2.0-dev-590", includedoc="Replaced by :func:`get_transport`") + + def commands(self, commands): + """ + Sends one or more gcode commands to the printer. + """ + if self._comm is None: + return + + if not isinstance(commands, (list, tuple)): + commands = [commands] + + for command in commands: + self._comm.sendCommand(command) + + def jog(self, axis, amount): + if not isinstance(axis, (str, unicode)): + raise ValueError("axis must be a string: {axis}".format(axis=axis)) + + axis = axis.lower() + if not axis in PrinterInterface.valid_axes: + raise ValueError("axis must be any of {axes}: {axis}".format(axes=", ".join(PrinterInterface.valid_axes), axis=axis)) + if not isinstance(amount, (int, long, float)): + raise ValueError("amount must be a valid number: {amount}".format(amount=amount)) + + printer_profile = self._printerProfileManager.get_current_or_default() + movement_speed = printer_profile["axes"][axis]["speed"] + self.commands(["G91", "G1 %s%.4f F%d" % (axis.upper(), amount, movement_speed), "G90"]) + + def home(self, axes): + if not isinstance(axes, (list, tuple)): + if isinstance(axes, (str, unicode)): + axes = [axes] + else: + raise ValueError("axes is neither a list nor a string: {axes}".format(axes=axes)) + + validated_axes = filter(lambda x: x in PrinterInterface.valid_axes, map(lambda x: x.lower(), axes)) + if len(axes) != len(validated_axes): + raise ValueError("axes contains invalid axes: {axes}".format(axes=axes)) + + self.commands(["G91", "G28 %s" % " ".join(map(lambda x: "%s0" % x.upper(), validated_axes)), "G90"]) + + def extrude(self, amount): + if not isinstance(amount, (int, long, float)): + raise ValueError("amount must be a valid number: {amount}".format(amount=amount)) + + printer_profile = self._printerProfileManager.get_current_or_default() + extrusion_speed = printer_profile["axes"]["e"]["speed"] + self.commands(["G91", "G1 E%s F%d" % (amount, extrusion_speed), "G90"]) + + def change_tool(self, tool): + if not isinstance(tool, int) or tool < 0: + raise ValueError("tool must be an integer >= 0: {tool}".format(tool, tool)) + + toolNum = int(tool[len("tool"):]) + self.commands("T%d" % toolNum) + + def set_temperature(self, heater, value): + if not PrinterInterface.valid_tool_regex.match(heater): + raise ValueError("heater must match \"tool[0-9]+\" or \"bed\": {heater}".format(type=heater)) + + if not isinstance(value, (int, long, float)) or value < 0: + raise ValueError("value must be a valid number >= 0: {value}".format(value=value)) + + if heater.startswith("tool"): + printer_profile = self._printerProfileManager.get_current_or_default() + extruder_count = printer_profile["extruder"]["count"] + if extruder_count > 1: + toolNum = int(heater[len("tool"):]) + self.commands("M104 T%d S%f" % (toolNum, value)) + else: + self.commands("M104 S%f" % value) + + elif heater == "bed": + self.commands("M140 S%f" % value) + + def set_temperature_offset(self, offsets=None): + if offsets is None: + offsets = dict() + + if not isinstance(offsets, dict): + raise ValueError("offsets must be a dict") + + validated_keys = filter(lambda x: PrinterInterface.valid_tool_regex.match(x), offsets.keys()) + validated_values = filter(lambda x: isinstance(value, (int, long, float)), offsets.values()) + + if len(validated_keys) != len(offsets): + raise ValueError("offsets contains invalid keys: {offsets}".format(offsets=offsets)) + if len(validated_values) != len(offsets): + raise ValueError("offsets contains invalid values: {offsets}".format(offsets=offsets)) + + if self._comm is None: + return + + tool, bed = self._comm.getOffsets() + + validatedOffsets = dict() + + for key in offsets: + value = offsets[key] + if key == "bed": + bed = value + validatedOffsets[key] = value + elif key.startswith("tool"): + toolNum = int(key[len("tool"):]) + tool[toolNum] = value + validatedOffsets[key] = value + + self._comm.setTemperatureOffset(tool, bed) + self._stateMonitor.set_temp_offsets(validatedOffsets) + + def _convert_rate_value(self, factor, min=0, max=200): + if not isinstance(factor, (int, float, long)): + raise ValueError("factor is not a number") + + if isinstance(factor, float): + factor = int(factor * 100.0) + + if factor < min or factor > max: + raise ValueError("factor must be a value between %f and %f" % (min, max)) + + return factor + + def feed_rate(self, factor): + factor = self._convert_rate_value(factor, min=50, max=200) + self.commands("M220 S%d" % factor) + + def flow_rate(self, factor): + factor = self._convert_rate_value(factor, min=75, max=125) + self.commands("M221 S%d" % factor) + + def select_file(self, path, sd, printAfterSelect=False): + if self._comm is None or (self._comm.isBusy() or self._comm.isStreaming()): + self._logger.info("Cannot load file: printer not connected or currently busy") + return + + self._printAfterSelect = printAfterSelect + self._comm.selectFile("/" + path if sd else path, sd) + self._setProgressData(0, None, None, None) + self._setCurrentZ(None) + + def unselect_file(self): + if self._comm is not None and (self._comm.isBusy() or self._comm.isStreaming()): + return + + self._comm.unselectFile() + self._setProgressData(0, None, None, None) + self._setCurrentZ(None) + + def start_print(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._timeEstimationData = TimeEstimationHelper() + self._lastProgressReport = None + self._setCurrentZ(None) + self._comm.startPrint() + + def toggle_pause_print(self): + """ + Pause the current printjob. + """ + if self._comm is None: + return + + self._comm.setPause(not self._comm.isPaused()) + + def cancel_print(self): + """ + Cancel the current printjob. + """ + if self._comm is None: + return + + self._comm.cancelPrint() + + # 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._fileManager.log_print(FileDestinations.SDCARD if self._selectedFile["sd"] else FileDestinations.LOCAL, self._selectedFile["filename"], time.time(), self._comm.getPrintTime(), False, self._printerProfileManager.get_current_or_default()["id"]) + payload = { + "file": self._selectedFile["filename"], + "origin": FileDestinations.LOCAL + } + if self._selectedFile["sd"]: + payload["origin"] = FileDestinations.SDCARD + eventManager().fire(Events.PRINT_FAILED, payload) + + def get_state_string(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 get_current_data(self): + return self._stateMonitor.get_current_data() + + def get_current_job(self): + currentData = self._stateMonitor.get_current_data() + return currentData["job"] + + def get_current_temperatures(self): + if self._comm is not None: + tempOffset, bedTempOffset = self._comm.getOffsets() + else: + tempOffset = {} + bedTempOffset = None + + result = {} + if self._temp is not None: + for tool in self._temp.keys(): + result["tool%d" % tool] = { + "actual": self._temp[tool][0], + "target": self._temp[tool][1], + "offset": tempOffset[tool] if tool in tempOffset.keys() and tempOffset[tool] is not None else 0 + } + if self._bedTemp is not None: + result["bed"] = { + "actual": self._bedTemp[0], + "target": self._bedTemp[1], + "offset": bedTempOffset + } + + return result + + def get_temperature_history(self): + return self._temps + + def get_current_connection(self): + if self._comm is None: + return "Closed", None, None, None + + port, baudrate = self._comm.getConnection() + printer_profile = self._printerProfileManager.get_current_or_default() + return self._comm.getStateString(), port, baudrate, printer_profile + + def is_closed_or_error(self): + return self._comm is None or self._comm.isClosedOrError() + + def is_operational(self): + return self._comm is not None and self._comm.isOperational() + + def is_printing(self): + return self._comm is not None and self._comm.isPrinting() + + def is_paused(self): + return self._comm is not None and self._comm.isPaused() + + def is_error(self): + return self._comm is not None and self._comm.isError() + + def is_ready(self): + return self.is_operational() and not self._comm.isStreaming() + + def is_sd_ready(self): + if not settings().getBoolean(["feature", "sdSupport"]) or self._comm is None: + return False + else: + return self._comm.isSdReady() + + #~~ sd file handling + + def get_sd_files(self): + if self._comm is None or not self._comm.isSdReady(): + return [] + return map(lambda x: (x[0][1:], x[1]), self._comm.getSdFiles()) + + def add_sd_file(self, filename, absolutePath, streamingFinishedCallback): + if not self._comm or self._comm.isBusy() or not self._comm.isSdReady(): + self._logger.error("No connection to printer or printer is busy") + return + + self._streamingFinishedCallback = streamingFinishedCallback + + self.refresh_sd_files(blocking=True) + existingSdFiles = map(lambda x: x[0], self._comm.getSdFiles()) + + remoteName = util.get_dos_filename(filename, existing_filenames=existingSdFiles, extension="gco") + self._timeEstimationData = TimeEstimationHelper() + self._comm.startFileTransfer(absolutePath, filename, "/" + remoteName) + + return remoteName + + def delete_sd_file(self, filename): + if not self._comm or not self._comm.isSdReady(): + return + self._comm.deleteSdFile("/" + filename) + + def init_sd_card(self): + if not self._comm or self._comm.isSdReady(): + return + self._comm.initSdCard() + + def release_sd_card(self): + if not self._comm or not self._comm.isSdReady(): + return + self._comm.releaseSdCard() + + def refresh_sd_files(self, blocking=False): + """ + Refreshs the list of file stored on the SD card attached to printer (if available and printer communication + available). Optional blocking parameter allows making the method block (max 10s) until the file list has been + received (and can be accessed via self._comm.getSdFiles()). Defaults to an asynchronous operation. + """ + if not self._comm or not self._comm.isSdReady(): + return + self._sdFilelistAvailable.clear() + self._comm.refreshSdFiles() + if blocking: + self._sdFilelistAvailable.wait(10000) + + #~~ state monitoring + + def _setCurrentZ(self, currentZ): + self._currentZ = currentZ + self._stateMonitor.set_current_z(self._currentZ) + + def _setState(self, state): + self._state = state + self._stateMonitor.set_state({"text": self.get_state_string(), "flags": self._getStateFlags()}) + + def _addLog(self, log): + self._log.append(log) + self._stateMonitor.add_log(log) + + def _addMessage(self, message): + self._messages.append(message) + self._stateMonitor.add_message(message) + + def _estimateTotalPrintTime(self, progress, printTime): + if not progress or not printTime or not self._timeEstimationData: + return None + + else: + newEstimate = printTime / progress + self._timeEstimationData.update(newEstimate) + + result = None + if self._timeEstimationData.is_stable(): + result = self._timeEstimationData.average_total_rolling + + return result + + def _setProgressData(self, progress, filepos, printTime, cleanedPrintTime): + estimatedTotalPrintTime = self._estimateTotalPrintTime(progress, cleanedPrintTime) + totalPrintTime = estimatedTotalPrintTime + + if self._selectedFile and "estimatedPrintTime" in self._selectedFile and self._selectedFile["estimatedPrintTime"]: + statisticalTotalPrintTime = self._selectedFile["estimatedPrintTime"] + if progress and cleanedPrintTime: + if estimatedTotalPrintTime is None: + totalPrintTime = statisticalTotalPrintTime + else: + if progress < 0.5: + sub_progress = progress * 2 + else: + sub_progress = 1.0 + totalPrintTime = (1 - sub_progress) * statisticalTotalPrintTime + sub_progress * estimatedTotalPrintTime + + self._progress = progress + self._printTime = printTime + self._printTimeLeft = totalPrintTime - cleanedPrintTime if (totalPrintTime is not None and cleanedPrintTime is not None) else None + + self._stateMonitor.set_progress({ + "completion": self._progress * 100 if self._progress is not None else None, + "filepos": filepos, + "printTime": int(self._printTime) if self._printTime is not None else None, + "printTimeLeft": int(self._printTimeLeft) if self._printTimeLeft is not None else None + }) + + if progress: + progress_int = int(progress * 100) + if self._lastProgressReport != progress_int: + self._lastProgressReport = progress_int + self._reportPrintProgressToPlugins(progress_int) + + + def _addTemperatureData(self, temp, bedTemp): + currentTimeUtc = int(time.time()) + + data = { + "time": currentTimeUtc + } + for tool in temp.keys(): + data["tool%d" % tool] = { + "actual": temp[tool][0], + "target": temp[tool][1] + } + if bedTemp is not None and isinstance(bedTemp, tuple): + data["bed"] = { + "actual": bedTemp[0], + "target": bedTemp[1] + } + + self._temps.append(data) + + self._temp = temp + self._bedTemp = bedTemp + + self._stateMonitor.add_temperature(data) + + def _setJobData(self, filename, filesize, sd): + if filename is not None: + if sd: + path_in_storage = filename[1:] + path_on_disk = None + else: + path_in_storage = self._fileManager.path_in_storage(FileDestinations.LOCAL, filename) + path_on_disk = self._fileManager.path_on_disk(FileDestinations.LOCAL, filename) + self._selectedFile = { + "filename": path_in_storage, + "filesize": filesize, + "sd": sd, + "estimatedPrintTime": None + } + else: + self._selectedFile = None + self._stateMonitor.set_job_data({ + "file": { + "name": None, + "origin": None, + "size": None, + "date": None + }, + "estimatedPrintTime": None, + "averagePrintTime": None, + "lastPrintTime": None, + "filament": None, + }) + return + + estimatedPrintTime = None + lastPrintTime = None + averagePrintTime = None + date = None + filament = None + if path_on_disk: + # Use a string for mtime because it could be float and the + # javascript needs to exact match + if not sd: + date = int(os.stat(path_on_disk).st_ctime) + + try: + fileData = self._fileManager.get_metadata(FileDestinations.SDCARD if sd else FileDestinations.LOCAL, path_on_disk) + except: + fileData = None + if fileData is not None: + if "analysis" in fileData: + if estimatedPrintTime is None and "estimatedPrintTime" in fileData["analysis"]: + estimatedPrintTime = fileData["analysis"]["estimatedPrintTime"] + if "filament" in fileData["analysis"].keys(): + filament = fileData["analysis"]["filament"] + if "statistics" in fileData: + printer_profile = self._printerProfileManager.get_current_or_default()["id"] + if "averagePrintTime" in fileData["statistics"] and printer_profile in fileData["statistics"]["averagePrintTime"]: + averagePrintTime = fileData["statistics"]["averagePrintTime"][printer_profile] + if "lastPrintTime" in fileData["statistics"] and printer_profile in fileData["statistics"]["lastPrintTime"]: + lastPrintTime = fileData["statistics"]["lastPrintTime"][printer_profile] + + if averagePrintTime is not None: + self._selectedFile["estimatedPrintTime"] = averagePrintTime + elif estimatedPrintTime is not None: + # TODO apply factor which first needs to be tracked! + self._selectedFile["estimatedPrintTime"] = estimatedPrintTime + + self._stateMonitor.set_job_data({ + "file": { + "name": path_in_storage, + "origin": FileDestinations.SDCARD if sd else FileDestinations.LOCAL, + "size": filesize, + "date": date + }, + "estimatedPrintTime": estimatedPrintTime, + "averagePrintTime": averagePrintTime, + "lastPrintTime": lastPrintTime, + "filament": filament, + }) + + def _sendInitialStateUpdate(self, callback): + try: + data = self._stateMonitor.get_current_data() + data.update({ + "temps": list(self._temps), + "logs": list(self._log), + "messages": list(self._messages) + }) + callback.on_printer_send_initial_data(data) + except Exception, err: + import sys + sys.stderr.write("ERROR: %s\n" % str(err)) + pass + + def _getStateFlags(self): + return { + "operational": self.is_operational(), + "printing": self.is_printing(), + "closedOrError": self.is_closed_or_error(), + "error": self.is_error(), + "paused": self.is_paused(), + "ready": self.is_ready(), + "sdReady": self.is_sd_ready() + } + + #~~ comm.MachineComPrintCallback implementation + + def on_comm_log(self, message): + """ + Callback method for the comm object, called upon log output. + """ + self._addLog(message) + + def on_comm_temperature_update(self, temp, bedTemp): + self._addTemperatureData(temp, bedTemp) + + def on_comm_state_change(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._fileManager.log_print(FileDestinations.SDCARD if self._selectedFile["sd"] else FileDestinations.LOCAL, self._selectedFile["filename"], time.time(), self._comm.getPrintTime(), True, self._printerProfileManager.get_current_or_default()["id"]) + elif state == self._comm.STATE_CLOSED or state == self._comm.STATE_ERROR or state == self._comm.STATE_CLOSED_WITH_ERROR: + self._fileManager.log_print(FileDestinations.SDCARD if self._selectedFile["sd"] else FileDestinations.LOCAL, self._selectedFile["filename"], time.time(), self._comm.getPrintTime(), False, self._printerProfileManager.get_current_or_default()["id"]) + self._analysisQueue.resume() # printing done, put those cpu cycles to good use + elif self._comm is not None and state == self._comm.STATE_PRINTING: + self._analysisQueue.pause() # do not analyse files while printing + + self._setState(state) + + def on_comm_message(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 on_comm_progress(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.getCleanedPrintTime()) + + def on_comm_z_change(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(Events.Z_CHANGE, {"new": newZ, "old": oldZ}) + + self._setCurrentZ(newZ) + + def on_comm_sd_state_change(self, sdReady): + self._stateMonitor.set_state({"text": self.get_state_string(), "flags": self._getStateFlags()}) + + def on_comm_sd_files(self, files): + eventManager().fire(Events.UPDATED_FILES, {"type": "gcode"}) + self._sdFilelistAvailable.set() + + def on_comm_file_selected(self, filename, filesize, sd): + self._setJobData(filename, filesize, sd) + self._stateMonitor.set_state({"text": self.get_state_string(), "flags": self._getStateFlags()}) + + if self._printAfterSelect: + self.start_print() + + def on_comm_print_job_done(self): + self._setProgressData(1.0, self._selectedFile["filesize"], self._comm.getPrintTime(), 0) + self._stateMonitor.set_state({"text": self.get_state_string(), "flags": self._getStateFlags()}) + + def on_comm_file_transfer_started(self, filename, filesize): + self._sdStreaming = True + + self._setJobData(filename, filesize, True) + self._setProgressData(0.0, 0, 0, None) + self._stateMonitor.set_state({"text": self.get_state_string(), "flags": self._getStateFlags()}) + + def on_comm_file_transfer_done(self, filename): + self._sdStreaming = False + + if self._streamingFinishedCallback is not None: + # in case of SD files, both filename and absolutePath are the same, so we set the (remote) filename for + # both parameters + self._streamingFinishedCallback(filename, filename, FileDestinations.SDCARD) + + self._setCurrentZ(None) + self._setJobData(None, None, None) + self._setProgressData(None, None, None, None) + self._stateMonitor.set_state({"text": self.get_state_string(), "flags": self._getStateFlags()}) + + def on_comm_received_registered_message(self, command, output): + self._sendFeedbackCommandOutput(command, output) + + def on_comm_force_disconnect(self): + self.disconnect() + + +class StateMonitor(object): + def __init__(self, interval=0.5, on_update=None, on_add_temperature=None, on_add_log=None, on_add_message=None): + self._interval = interval + self._update_callback = on_update + self._on_add_temperature = on_add_temperature + self._on_add_log = on_add_log + self._on_add_message = on_add_message + + self._state = None + self._job_data = None + self._gcode_data = None + self._sd_upload_data = None + self._current_z = None + self._progress = None + + self._offsets = {} + + self._change_event = threading.Event() + self._state_lock = threading.Lock() + + self._last_update = time.time() + self._worker = threading.Thread(target=self._work) + self._worker.daemon = True + self._worker.start() + + def reset(self, state=None, job_data=None, progress=None, current_z=None): + self.set_state(state) + self.set_job_data(job_data) + self.set_progress(progress) + self.set_current_z(current_z) + + def add_temperature(self, temperature): + self._on_add_temperature(temperature) + self._change_event.set() + + def add_log(self, log): + self._on_add_log(log) + self._change_event.set() + + def add_message(self, message): + self._on_add_message(message) + self._change_event.set() + + def set_current_z(self, current_z): + self._current_z = current_z + self._change_event.set() + + def set_state(self, state): + with self._state_lock: + self._state = state + self._change_event.set() + + def set_job_data(self, job_data): + self._job_data = job_data + self._change_event.set() + + def set_progress(self, progress): + self._progress = progress + self._change_event.set() + + def set_temp_offsets(self, offsets): + self._offsets = offsets + self._change_event.set() + + def _work(self): + while True: + self._change_event.wait() + + with self._state_lock: + now = time.time() + delta = now - self._last_update + additional_wait_time = self._interval - delta + if additional_wait_time > 0: + time.sleep(additional_wait_time) + + data = self.get_current_data() + self._update_callback(data) + self._last_update = time.time() + self._change_event.clear() + + def get_current_data(self): + return { + "state": self._state, + "job": self._job_data, + "currentZ": self._current_z, + "progress": self._progress, + "offsets": self._offsets + } + + diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index 981cafcb..e1c2ce72 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -43,8 +43,9 @@ admin_permission = Permission(RoleNeed("admin")) user_permission = Permission(RoleNeed("user")) # only import the octoprint stuff down here, as it might depend on things defined above to be initialized already -from octoprint.printer import Printer, getConnectionOptions +from octoprint.printer import get_connection_options from octoprint.printer.profile import PrinterProfileManager +from octoprint.printer.standard import Printer from octoprint.settings import settings import octoprint.users as users import octoprint.events as events @@ -673,7 +674,7 @@ class Server(): if settings().getBoolean(["serial", "autoconnect"]): (port, baudrate) = settings().get(["serial", "port"]), settings().getInt(["serial", "baudrate"]) printer_profile = printerProfileManager.get_default() - connectionOptions = getConnectionOptions() + connectionOptions = get_connection_options() if port in connectionOptions["ports"]: printer.connect(port=port, baudrate=baudrate, profile=printer_profile["id"] if "id" in printer_profile else "_default") diff --git a/src/octoprint/server/api/connection.py b/src/octoprint/server/api/connection.py index 911ffe4b..7f858c7d 100644 --- a/src/octoprint/server/api/connection.py +++ b/src/octoprint/server/api/connection.py @@ -8,7 +8,7 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms from flask import request, jsonify, make_response from octoprint.settings import settings -from octoprint.printer import getConnectionOptions +from octoprint.printer import get_connection_options from octoprint.server import printer, printerProfileManager, NO_CONTENT from octoprint.server.api import api from octoprint.server.util.flask import restricted_access, get_json_command_from_request @@ -17,7 +17,7 @@ import octoprint.util as util @api.route("/connection", methods=["GET"]) def connectionState(): - state, port, baudrate, printer_profile = printer.getCurrentConnection() + state, port, baudrate, printer_profile = printer.get_current_connection() current = { "state": state, "port": port, @@ -41,7 +41,7 @@ def connectionCommand(): return response if command == "connect": - connection_options = getConnectionOptions() + connection_options = get_connection_options() port = None baudrate = None @@ -72,7 +72,7 @@ def connectionCommand(): return NO_CONTENT def _get_options(): - connection_options = getConnectionOptions() + connection_options = get_connection_options() profile_options = printerProfileManager.get_all() default_profile = printerProfileManager.get_default() diff --git a/src/octoprint/server/api/files.py b/src/octoprint/server/api/files.py index 8a1e3b7e..0f6d292d 100644 --- a/src/octoprint/server/api/files.py +++ b/src/octoprint/server/api/files.py @@ -53,7 +53,7 @@ def _getFileDetails(origin, filename): def _getFileList(origin, filter=None): if origin == FileDestinations.SDCARD: - sdFileList = printer.getSdFiles() + sdFileList = printer.get_sd_files() files = [] if sdFileList is not None: @@ -117,7 +117,7 @@ def _getFileList(origin, filter=None): def _verifyFileExists(origin, filename): if origin == FileDestinations.SDCARD: - return filename in map(lambda x: x[0], printer.getSdFiles()) + return filename in map(lambda x: x[0], printer.get_sd_files()) else: return fileManager.file_exists(origin, filename) @@ -150,15 +150,15 @@ def uploadGcodeFile(target): if sd: # validate that all preconditions for SD upload are met before attempting it - if not (printer.isOperational() and not (printer.isPrinting() or printer.isPaused())): + if not (printer.is_operational() and not (printer.is_printing() or printer.is_paused())): return make_response("Can not upload to SD card, printer is either not operational or already busy", 409) - if not printer.isSdReady(): + if not printer.is_sd_ready(): return make_response("Can not upload to SD card, not yet initialized", 409) # determine current job currentFilename = None currentOrigin = None - currentJob = printer.getCurrentJob() + currentJob = printer.get_current_job() if currentJob is not None and "file" in currentJob.keys(): currentJobFile = currentJob["file"] if "name" in currentJobFile.keys() and "origin" in currentJobFile.keys(): @@ -174,7 +174,7 @@ def uploadGcodeFile(target): return make_response("Can not upload file %s, wrong format?" % upload.filename, 415) # prohibit overwriting currently selected file while it's being printed - if futureFilename == currentFilename and target == currentOrigin and printer.isPrinting() or printer.isPaused(): + if futureFilename == currentFilename and target == currentOrigin and printer.is_printing() or printer.is_paused(): return make_response("Trying to overwrite file that is currently being printed: %s" % currentFilename, 409) def fileProcessingFinished(filename, absFilename, destination): @@ -186,7 +186,7 @@ def uploadGcodeFile(target): """ if destination == FileDestinations.SDCARD and octoprint.filemanager.valid_file_type(filename, "gcode"): - return filename, printer.addSdFile(filename, absFilename, selectAndOrPrint) + return filename, printer.add_sd_file(filename, absFilename, selectAndOrPrint) else: selectAndOrPrint(filename, absFilename, destination) return filename @@ -201,7 +201,7 @@ def uploadGcodeFile(target): exact file is already selected, such reloading it. """ if octoprint.filemanager.valid_file_type(added_file, "gcode") and (selectAfterUpload or printAfterSelect or (currentFilename == filename and currentOrigin == destination)): - printer.selectFile(absFilename, destination == FileDestinations.SDCARD, printAfterSelect) + printer.select_file(absFilename, destination == FileDestinations.SDCARD, printAfterSelect) added_file = fileManager.add_file(FileDestinations.LOCAL, upload.filename, upload, allow_overwrite=True) if added_file is None: @@ -284,7 +284,7 @@ def gcodeFileCommand(filename, target): # selects/loads a file printAfterLoading = False if "print" in data.keys() and data["print"] in valid_boolean_trues: - if not printer.isOperational(): + if not printer.is_operational(): return make_response("Printer is not operational, cannot directly start printing", 409) printAfterLoading = True @@ -294,7 +294,7 @@ def gcodeFileCommand(filename, target): sd = True else: filenameToSelect = fileManager.path_on_disk(target, filename) - printer.selectFile(filenameToSelect, sd, printAfterLoading) + printer.select_file(filenameToSelect, sd, printAfterLoading) elif command == "slice": if "slicer" in data.keys(): @@ -312,7 +312,7 @@ def gcodeFileCommand(filename, target): if not octoprint.filemanager.valid_file_type(filename, type="stl"): return make_response("Cannot slice {filename}, not an STL file".format(**locals()), 415) - if slicer_instance.get_slicer_properties()["same_device"] and (printer.isPrinting() or printer.isPaused()): + if slicer_instance.get_slicer_properties()["same_device"] and (printer.is_printing() or printer.is_paused()): # slicer runs on same device as OctoPrint, slicing while printing is hence disabled return make_response("Cannot slice on {slicer} while printing due to performance reasons".format(**locals()), 409) @@ -326,7 +326,7 @@ def gcodeFileCommand(filename, target): # prohibit overwriting the file that is currently being printed currentOrigin, currentFilename = _getCurrentFile() - if currentFilename == gcode_name and currentOrigin == target and (printer.isPrinting() or printer.isPaused()): + if currentFilename == gcode_name and currentOrigin == target and (printer.is_printing() or printer.is_paused()): make_response("Trying to slice into file that is currently being printed: %s" % gcode_name, 409) if "profile" in data.keys() and data["profile"]: @@ -349,13 +349,13 @@ def gcodeFileCommand(filename, target): select_after_slicing = False if "select" in data.keys() and data["select"] in valid_boolean_trues: - if not printer.isOperational(): + if not printer.is_operational(): return make_response("Printer is not operational, cannot directly select for printing", 409) select_after_slicing = True print_after_slicing = False if "print" in data.keys() and data["print"] in valid_boolean_trues: - if not printer.isOperational(): + if not printer.is_operational(): return make_response("Printer is not operational, cannot directly start printing", 409) select_after_slicing = print_after_slicing = True @@ -372,7 +372,7 @@ def gcodeFileCommand(filename, target): sd = True else: filenameToSelect = fileManager.path_on_disk(target, gcode_name) - printer.selectFile(filenameToSelect, sd, print_after_slicing) + printer.select_file(filenameToSelect, sd, print_after_slicing) ok, result = fileManager.slice(slicer, target, filename, target, gcode_name, profile=profile, @@ -414,7 +414,7 @@ def deleteGcodeFile(filename, target): # prohibit deleting files that are currently in use currentOrigin, currentFilename = _getCurrentFile() - if currentFilename == filename and currentOrigin == target and (printer.isPrinting() or printer.isPaused()): + if currentFilename == filename and currentOrigin == target and (printer.is_printing() or printer.is_paused()): make_response("Trying to delete file that is currently being printed: %s" % filename, 409) if (target, filename) in fileManager.get_busy_files(): @@ -422,18 +422,18 @@ def deleteGcodeFile(filename, target): # deselect the file if it's currently selected if currentFilename is not None and filename == currentFilename: - printer.unselectFile() + printer.unselect_file() # delete it if target == FileDestinations.SDCARD: - printer.deleteSdFile(filename) + printer.delete_sd_file(filename) else: fileManager.remove_file(target, filename) return NO_CONTENT def _getCurrentFile(): - currentJob = printer.getCurrentJob() + currentJob = printer.get_current_job() if currentJob is not None and "file" in currentJob.keys() and "name" in currentJob["file"] and "origin" in currentJob["file"]: return currentJob["file"]["origin"], currentJob["file"]["name"] else: diff --git a/src/octoprint/server/api/job.py b/src/octoprint/server/api/job.py index ae57039f..b2d9b02e 100644 --- a/src/octoprint/server/api/job.py +++ b/src/octoprint/server/api/job.py @@ -16,7 +16,7 @@ import octoprint.util as util @api.route("/job", methods=["POST"]) @restricted_access def controlJob(): - if not printer.isOperational(): + if not printer.is_operational(): return make_response("Printer is not operational", 409) valid_commands = { @@ -30,30 +30,30 @@ def controlJob(): if response is not None: return response - activePrintjob = printer.isPrinting() or printer.isPaused() + activePrintjob = printer.is_printing() or printer.is_paused() if command == "start": if activePrintjob: return make_response("Printer already has an active print job, did you mean 'restart'?", 409) - printer.startPrint() + printer.start_print() elif command == "restart": - if not printer.isPaused(): + if not printer.is_paused(): return make_response("Printer does not have an active print job or is not paused", 409) - printer.startPrint() + printer.start_print() elif command == "pause": if not activePrintjob: return make_response("Printer is neither printing nor paused, 'pause' command cannot be performed", 409) - printer.togglePausePrint() + printer.toggle_pause_print() elif command == "cancel": if not activePrintjob: return make_response("Printer is neither printing nor paused, 'cancel' command cannot be performed", 409) - printer.cancelPrint() + printer.cancel_print() return NO_CONTENT @api.route("/job", methods=["GET"]) def jobState(): - currentData = printer.getCurrentData() + currentData = printer.get_current_data() return jsonify({ "job": currentData["job"], "progress": currentData["progress"], diff --git a/src/octoprint/server/api/printer.py b/src/octoprint/server/api/printer.py index 20e22b32..7299ad63 100644 --- a/src/octoprint/server/api/printer.py +++ b/src/octoprint/server/api/printer.py @@ -20,7 +20,7 @@ import octoprint.util as util @api.route("/printer", methods=["GET"]) def printerState(): - if not printer.isOperational(): + if not printer.is_operational(): return make_response("Printer is not operational", 409) # process excludes @@ -38,11 +38,11 @@ def printerState(): # add sd information if not "sd" in excludes and settings().getBoolean(["feature", "sdSupport"]): - result.update({"sd": {"ready": printer.isSdReady()}}) + result.update({"sd": {"ready": printer.is_sd_ready()}}) # add state information if not "state" in excludes: - state = printer.getCurrentData()["state"] + state = printer.get_current_data()["state"] result.update({"state": state}) return jsonify(result) @@ -54,7 +54,7 @@ def printerState(): @api.route("/printer/tool", methods=["POST"]) @restricted_access def printerToolCommand(): - if not printer.isOperational(): + if not printer.is_operational(): return make_response("Printer is not operational", 409) valid_commands = { @@ -78,7 +78,7 @@ def printerToolCommand(): if not tool.startswith("tool"): return make_response("Invalid tool for selection: %s" % tool, 400) - printer.changeTool(tool) + printer.change_tool(tool) ##~~ temperature elif command == "target": @@ -95,7 +95,7 @@ def printerToolCommand(): # perform the actual temperature commands for tool in validated_values.keys(): - printer.setTemperature(tool, validated_values[tool]) + printer.set_temperature(tool, validated_values[tool]) ##~~ temperature offset elif command == "offset": @@ -113,11 +113,11 @@ def printerToolCommand(): validated_values[tool] = value # set the offsets - printer.setTemperatureOffset(validated_values) + printer.set_temperature_offset(validated_values) ##~~ extrusion elif command == "extrude": - if printer.isPrinting(): + if printer.is_printing(): # do not extrude when a print job is running return make_response("Printer is currently printing", 409) @@ -131,7 +131,7 @@ def printerToolCommand(): if not isinstance(factor, (int, long, float)): return make_response("Not a number for flow rate: %r" % factor, 400) try: - printer.flowRate(factor) + printer.flow_rate(factor) except ValueError as e: return make_response("Invalid value for flow rate: %s" % e.message, 400) @@ -140,7 +140,7 @@ def printerToolCommand(): @api.route("/printer/tool", methods=["GET"]) def printerToolState(): - if not printer.isOperational(): + if not printer.is_operational(): return make_response("Printer is not operational", 409) def deleteBed(x): @@ -159,7 +159,7 @@ def printerToolState(): @api.route("/printer/bed", methods=["POST"]) @restricted_access def printerBedCommand(): - if not printer.isOperational(): + if not printer.is_operational(): return make_response("Printer is not operational", 409) valid_commands = { @@ -179,7 +179,7 @@ def printerBedCommand(): return make_response("Not a number: %r" % target, 400) # perform the actual temperature command - printer.setTemperature("bed", target) + printer.set_temperature("bed", target) ##~~ temperature offset elif command == "offset": @@ -192,14 +192,14 @@ def printerBedCommand(): return make_response("Offset not in range [-50, 50]: %f" % offset, 400) # set the offsets - printer.setTemperatureOffset({"bed": offset}) + printer.set_temperature_offset({"bed": offset}) return NO_CONTENT @api.route("/printer/bed", methods=["GET"]) def printerBedState(): - if not printer.isOperational(): + if not printer.is_operational(): return make_response("Printer is not operational", 409) def deleteTools(x): @@ -223,7 +223,7 @@ def printerBedState(): @api.route("/printer/printhead", methods=["POST"]) @restricted_access def printerPrintheadCommand(): - if not printer.isOperational() or printer.isPrinting(): + if not printer.is_operational() or printer.is_printing(): # do not jog when a print job is running or we don't have a connection return make_response("Printer is not operational or currently printing", 409) @@ -269,7 +269,7 @@ def printerPrintheadCommand(): if not isinstance(factor, (int, long, float)): return make_response("Not a number for feed rate: %r" % factor, 400) try: - printer.feedRate(factor) + printer.feed_rate(factor) except ValueError as e: return make_response("Invalid value for feed rate: %s" % e.message, 400) @@ -285,7 +285,7 @@ def printerSdCommand(): if not settings().getBoolean(["feature", "sdSupport"]): return make_response("SD support is disabled", 404) - if not printer.isOperational() or printer.isPrinting() or printer.isPaused(): + if not printer.is_operational() or printer.is_printing() or printer.is_paused(): return make_response("Printer is not operational or currently busy", 409) valid_commands = { @@ -298,11 +298,11 @@ def printerSdCommand(): return response if command == "init": - printer.initSdCard() + printer.init_sd_card() elif command == "refresh": - printer.refreshSdFiles() + printer.refresh_sd_files() elif command == "release": - printer.releaseSdCard() + printer.release_sd_card() return NO_CONTENT @@ -312,7 +312,7 @@ def printerSdState(): if not settings().getBoolean(["feature", "sdSupport"]): return make_response("SD support is disabled", 404) - return jsonify(ready=printer.isSdReady()) + return jsonify(ready=printer.is_sd_ready()) ##~~ Commands @@ -321,7 +321,7 @@ def printerSdState(): @api.route("/printer/command", methods=["POST"]) @restricted_access def printerCommand(): - if not printer.isOperational(): + if not printer.is_operational(): return make_response("Printer is not operational", 409) if not "application/json" in request.headers["Content-Type"]: @@ -364,13 +364,13 @@ def getCustomControls(): def _getTemperatureData(filter): - if not printer.isOperational(): + if not printer.is_operational(): return make_response("Printer is not operational", 409) - tempData = printer.getCurrentTemperatures() + tempData = printer.get_current_temperatures() if "history" in request.values.keys() and request.values["history"] in valid_boolean_trues: - tempHistory = printer.getTemperatureHistory() + tempHistory = printer.get_temperature_history() limit = 300 if "limit" in request.values.keys() and unicode(request.values["limit"]).isnumeric(): diff --git a/src/octoprint/server/api/settings.py b/src/octoprint/server/api/settings.py index fd875fce..5b00c830 100644 --- a/src/octoprint/server/api/settings.py +++ b/src/octoprint/server/api/settings.py @@ -12,7 +12,7 @@ from flask.exceptions import JSONBadRequest from octoprint.events import eventManager, Events from octoprint.settings import settings -from octoprint.printer import getConnectionOptions +from octoprint.printer import get_connection_options from octoprint.server import admin_permission from octoprint.server.api import api @@ -28,7 +28,7 @@ import octoprint.util def getSettings(): s = settings() - connectionOptions = getConnectionOptions() + connectionOptions = get_connection_options() data = { "api": { diff --git a/src/octoprint/server/util/sockjs.py b/src/octoprint/server/util/sockjs.py index 3d321b39..949a9efa 100644 --- a/src/octoprint/server/util/sockjs.py +++ b/src/octoprint/server/util/sockjs.py @@ -13,8 +13,10 @@ import octoprint.timelapse import octoprint.server from octoprint.events import Events +import octoprint.printer -class PrinterStateConnection(sockjs.tornado.SockJSConnection): + +class PrinterStateConnection(sockjs.tornado.SockJSConnection, octoprint.printer.PrinterCallback): def __init__(self, printer, fileManager, analysisQueue, userManager, eventManager, pluginManager, session): sockjs.tornado.SockJSConnection.__init__(self, session) @@ -49,7 +51,7 @@ class PrinterStateConnection(sockjs.tornado.SockJSConnection): # connected => update the API key, might be necessary if the client was left open while the server restarted self._emit("connected", {"apikey": octoprint.server.UI_API_KEY, "version": octoprint.server.VERSION, "display_version": octoprint.server.DISPLAY_VERSION}) - self._printer.registerCallback(self) + self._printer.register_callback(self) self._fileManager.register_slicingprogress_callback(self) octoprint.timelapse.registerCallback(self) self._pluginManager.register_client(self) @@ -62,7 +64,7 @@ class PrinterStateConnection(sockjs.tornado.SockJSConnection): def on_close(self): self._logger.info("Client connection closed: %s" % self._remoteAddress) - self._printer.unregisterCallback(self) + self._printer.unregister_callback(self) self._fileManager.unregister_slicingprogress_callback(self) octoprint.timelapse.unregisterCallback(self) self._pluginManager.unregister_client(self) @@ -74,7 +76,7 @@ class PrinterStateConnection(sockjs.tornado.SockJSConnection): def on_message(self, message): pass - def sendCurrentData(self, data): + def on_printer_send_current_data(self, data): # add current temperature, log and message backlogs to sent data with self._temperatureBacklogMutex: temperatures = self._temperatureBacklog @@ -92,7 +94,7 @@ class PrinterStateConnection(sockjs.tornado.SockJSConnection): if "job" in data and data["job"] is not None \ and "file" in data["job"] and "name" in data["job"]["file"] and "origin" in data["job"]["file"] \ and data["job"]["file"]["name"] is not None and data["job"]["file"]["origin"] is not None \ - and (self._printer.isPrinting() or self._printer.isPaused()): + and (self._printer.is_printing() or self._printer.is_paused()): busy_files.append(dict(origin=data["job"]["file"]["origin"], name=data["job"]["file"]["name"])) data.update({ @@ -103,13 +105,13 @@ class PrinterStateConnection(sockjs.tornado.SockJSConnection): }) self._emit("current", data) - def sendHistoryData(self, data): + def on_printer_send_initial_data(self, data): self._emit("history", data) def sendEvent(self, type, payload=None): self._emit("event", {"type": type, "payload": payload}) - def sendFeedbackCommandOutput(self, name, output): + def on_printer_received_registered_message(self, name, output): self._emit("feedbackCommandOutput", {"name": name, "output": output}) def sendTimelapseConfig(self, timelapseConfig): @@ -123,15 +125,15 @@ class PrinterStateConnection(sockjs.tornado.SockJSConnection): def sendPluginMessage(self, plugin, data): self._emit("plugin", dict(plugin=plugin, data=data)) - def addLog(self, data): + def on_printer_add_log(self, data): with self._logBacklogMutex: self._logBacklog.append(data) - def addMessage(self, data): + def on_printer_add_message(self, data): with self._messageBacklogMutex: self._messageBacklog.append(data) - def addTemperature(self, data): + def on_printer_add_temperature(self, data): with self._temperatureBacklogMutex: self._temperatureBacklog.append(data) diff --git a/src/octoprint/server/util/watchdog.py b/src/octoprint/server/util/watchdog.py index 4d4641e2..2375fbc2 100644 --- a/src/octoprint/server/util/watchdog.py +++ b/src/octoprint/server/util/watchdog.py @@ -43,7 +43,7 @@ class GcodeWatchdogHandler(watchdog.events.PatternMatchingEventHandler): # determine current job currentFilename = None currentOrigin = None - currentJob = self._printer.getCurrentJob() + currentJob = self._printer.get_current_job() if currentJob is not None and "file" in currentJob.keys(): currentJobFile = currentJob["file"] if "name" in currentJobFile.keys() and "origin" in currentJobFile.keys(): @@ -59,7 +59,7 @@ class GcodeWatchdogHandler(watchdog.events.PatternMatchingEventHandler): return # prohibit overwriting currently selected file while it's being printed - if futureFilename == currentFilename and currentOrigin == octoprint.filemanager.FileDestinations.LOCAL and self._printer.isPrinting() or self._printer.isPaused(): + if futureFilename == currentFilename and currentOrigin == octoprint.filemanager.FileDestinations.LOCAL and self._printer.is_printing() or self._printer.is_paused(): return added_file = self._file_manager.add_file(octoprint.filemanager.FileDestinations.LOCAL, diff --git a/src/octoprint/templates/overlays/dragndrop.jinja2 b/src/octoprint/templates/overlays/dragndrop.jinja2 index 9d07238e..91a74a38 100644 --- a/src/octoprint/templates/overlays/dragndrop.jinja2 +++ b/src/octoprint/templates/overlays/dragndrop.jinja2 @@ -2,12 +2,12 @@
{% if enableSdSupport %} -

{{ _('Upload locally') }}
+

{{ _('Upload locally') }}
-

{{ _('Upload to SD') }}
({{ _('SD not initialized') }})
+

{{ _('Upload to SD') }}
({{ _('SD not initialized') }})
{% else %} -

{{ _('Upload') }}
+

{{ _('Upload') }}
{% endif %}
diff --git a/src/octoprint/util/comm.py b/src/octoprint/util/comm.py index 3f00be97..1f90dc2b 100644 --- a/src/octoprint/util/comm.py +++ b/src/octoprint/util/comm.py @@ -223,15 +223,15 @@ class MachineCom(object): if settings().get(["feature", "sdSupport"]): self._sdFileList = False self._sdFiles = [] - self._callback.mcSdFiles([]) + self._callback.on_comm_sd_files([]) oldState = self.getStateString() self._state = newState self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString())) - self._callback.mcStateChange(newState) + self._callback.on_comm_state_change(newState) def _log(self, message): - self._callback.mcLog(message) + self._callback.on_comm_log(message) self._serialLogger.debug(message) def _addToLastLines(self, cmd): @@ -494,7 +494,7 @@ class MachineCom(object): self.sendCommand("M28 %s" % remoteFilename) eventManager().fire(Events.TRANSFER_STARTED, {"local": localFilename, "remote": remoteFilename}) - self._callback.mcFileTransferStarted(remoteFilename, self._currentFile.getFilesize()) + self._callback.on_comm_file_transfer_started(remoteFilename, self._currentFile.getFilesize()) def selectFile(self, filename, sd): if self.isBusy(): @@ -512,7 +512,7 @@ class MachineCom(object): "file": self._currentFile.getFilename(), "origin": self._currentFile.getFileLocation() }) - self._callback.mcFileSelected(filename, self._currentFile.getFilesize(), False) + self._callback.on_comm_file_selected(filename, self._currentFile.getFilesize(), False) def unselectFile(self): if self.isBusy(): @@ -520,7 +520,7 @@ class MachineCom(object): self._currentFile = None eventManager().fire(Events.FILE_DESELECTED) - self._callback.mcFileSelected(None, None, False) + self._callback.on_comm_file_selected(None, None, False) def cancelPrint(self): if not self.isOperational() or self.isStreaming(): @@ -625,7 +625,7 @@ class MachineCom(object): if settings().getBoolean(["feature", "sdAlwaysAvailable"]): self._sdAvailable = True self.refreshSdFiles() - self._callback.mcSdStateChange(self._sdAvailable) + self._callback.on_comm_sd_state_change(self._sdAvailable) def releaseSdCard(self): if not self.isOperational() or (self.isBusy() and self.isSdFileSelected()): @@ -636,8 +636,8 @@ class MachineCom(object): self._sdAvailable = False self._sdFiles = [] - self._callback.mcSdStateChange(self._sdAvailable) - self._callback.mcSdFiles(self._sdFiles) + self._callback.on_comm_sd_state_change(self._sdAvailable) + self._callback.on_comm_sd_files(self._sdFiles) ##~~ communication monitoring and handling @@ -761,7 +761,7 @@ class MachineCom(object): self.setPause(False) elif action_command == "disconnect": self._log("Disconnecting on request of the printer...") - self._callback.mcForceDisconnect() + self._callback.on_comm_force_disconnect() else: for hook in self._printer_action_hooks: self._printer_action_hooks[hook](self, line, action_command) @@ -803,7 +803,7 @@ class MachineCom(object): ##~~ Temperature processing if ' T:' in line or line.startswith('T:') or ' T0:' in line or line.startswith('T0:'): self._processTemperatures(line) - self._callback.mcTempUpdate(self._temp, self._bedTemp) + self._callback.on_comm_temperature_update(self._temp, self._bedTemp) elif supportRepetierTargetTemp and ('TargetExtr' in line or 'TargetBed' in line): matchExtr = self._regex_repetierTempExtr.match(line) @@ -818,7 +818,7 @@ class MachineCom(object): self._temp[toolNum] = (actual, target) else: self._temp[toolNum] = (None, target) - self._callback.mcTempUpdate(self._temp, self._bedTemp) + self._callback.on_comm_temperature_update(self._temp, self._bedTemp) except ValueError: pass elif matchBed is not None: @@ -829,7 +829,7 @@ class MachineCom(object): self._bedTemp = (actual, target) else: self._bedTemp = (None, target) - self._callback.mcTempUpdate(self._temp, self._bedTemp) + self._callback.on_comm_temperature_update(self._temp, self._bedTemp) except ValueError: pass @@ -842,7 +842,7 @@ class MachineCom(object): elif 'SD init fail' in line or 'volume.init failed' in line or 'openRoot failed' in line: self._sdAvailable = False self._sdFiles = [] - self._callback.mcSdStateChange(self._sdAvailable) + self._callback.on_comm_sd_state_change(self._sdAvailable) elif 'Not SD printing' in line: if self.isSdFileSelected() and self.isPrinting(): # something went wrong, printer is reporting that we actually are not printing right now... @@ -851,18 +851,18 @@ class MachineCom(object): elif 'SD card ok' in line and not self._sdAvailable: self._sdAvailable = True self.refreshSdFiles() - self._callback.mcSdStateChange(self._sdAvailable) + self._callback.on_comm_sd_state_change(self._sdAvailable) elif 'Begin file list' in line: self._sdFiles = [] self._sdFileList = True elif 'End file list' in line: self._sdFileList = False - self._callback.mcSdFiles(self._sdFiles) + self._callback.on_comm_sd_files(self._sdFiles) elif 'SD printing byte' in line: # answer to M27, at least on Marlin, Repetier and Sprinter: "SD printing byte %d/%d" match = self._regex_sdPrintingByte.search(line) self._currentFile.setFilepos(int(match.group(1))) - self._callback.mcProgress() + self._callback.on_comm_progress() elif 'File opened' in line: # answer to M23, at least on Marlin, Repetier and Sprinter: "File opened:%s Size:%d" match = self._regex_sdFileOpened.search(line) @@ -875,7 +875,7 @@ class MachineCom(object): elif 'File selected' in line: # final answer to M23, at least on Marlin, Repetier and Sprinter: "File selected" if self._currentFile is not None: - self._callback.mcFileSelected(self._currentFile.getFilename(), self._currentFile.getFilesize(), True) + self._callback.on_comm_file_selected(self._currentFile.getFilename(), self._currentFile.getFilesize(), True) eventManager().fire(Events.FILE_SELECTED, { "file": self._currentFile.getFilename(), "origin": self._currentFile.getFileLocation() @@ -887,7 +887,7 @@ class MachineCom(object): elif 'Done printing file' in line: # printer is reporting file finished printing self._sdFilePos = 0 - self._callback.mcPrintjobDone() + self._callback.on_comm_print_job_done() self._changeState(self.STATE_OPERATIONAL) eventManager().fire(Events.PRINT_DONE, { "file": self._currentFile.getFilename(), @@ -904,7 +904,7 @@ class MachineCom(object): and not line.startswith('Resend:') \ and line != 'echo:Unknown command:""\n' \ and self.isOperational(): - self._callback.mcMessage(line) + self._callback.on_comm_message(line) ##~~ Parsing for feedback commands if feedbackControls: @@ -922,7 +922,7 @@ class MachineCom(object): formatFunction = unicode.format if formatFunction is not None: - self._callback.mcReceivedRegisteredMessage(name, formatFunction(template, *(match.groups("n/a")))) + self._callback.on_comm_received_registered_message(name, formatFunction(template, *(match.groups("n/a")))) except: if not name in feedbackErrors: self._logger.info("Something went wrong with feedbackControl \"%s\": " % name, exc_info=True) @@ -1171,7 +1171,7 @@ class MachineCom(object): self._currentFile = None self._changeState(self.STATE_OPERATIONAL) - self._callback.mcFileTransferDone(remote) + self._callback.on_comm_file_transfer_done(remote) eventManager().fire(Events.TRANSFER_DONE, payload) self.refreshSdFiles() else: @@ -1181,7 +1181,7 @@ class MachineCom(object): "origin": self._currentFile.getFileLocation(), "time": self.getPrintTime() } - self._callback.mcPrintjobDone() + self._callback.on_comm_print_job_done() self._changeState(self.STATE_OPERATIONAL) eventManager().fire(Events.PRINT_DONE, payload) @@ -1193,7 +1193,7 @@ class MachineCom(object): line = self._getNext() if line is not None: self._sendCommand(line, True) - self._callback.mcProgress() + self._callback.on_comm_progress() def _handleResendRequest(self, line): lineToResend = None @@ -1325,7 +1325,7 @@ class MachineCom(object): z = float(match.group(1)) if self._currentZ != z: self._currentZ = z - self._callback.mcZChange(z) + self._callback.on_comm_z_change(z) except ValueError: pass return cmd @@ -1419,43 +1419,46 @@ class MachineCom(object): ### MachineCom callback ################################################################################################ class MachineComPrintCallback(object): - def mcLog(self, message): + def on_comm_log(self, message): pass - def mcTempUpdate(self, temp, bedTemp): + def on_comm_temperature_update(self, temp, bedTemp): pass - def mcStateChange(self, state): + def on_comm_state_change(self, state): pass - def mcMessage(self, message): + def on_comm_message(self, message): pass - def mcProgress(self): + def on_comm_progress(self): pass - def mcZChange(self, newZ): + def on_comm_print_job_done(self): pass - def mcFileSelected(self, filename, filesize, sd): + def on_comm_z_change(self, newZ): pass - def mcSdStateChange(self, sdReady): + def on_comm_file_selected(self, filename, filesize, sd): pass - def mcSdFiles(self, files): + def on_comm_sd_state_change(self, sdReady): pass - def mcSdPrintingDone(self): + def on_comm_sd_files(self, files): pass - def mcFileTransferStarted(self, filename, filesize): + def on_comm_file_transfer_started(self, filename, filesize): pass - def mcReceivedRegisteredMessage(self, command, message): + def on_comm_file_transfer_done(self, filename): pass - def mcForceDisconnect(self): + def on_comm_received_registered_message(self, command, message): + pass + + def on_comm_force_disconnect(self): pass ### Printing file information classes ################################################################################## diff --git a/src/octoprint/util/virtual.py b/src/octoprint/util/virtual.py index 90f871c9..51bb253d 100644 --- a/src/octoprint/util/virtual.py +++ b/src/octoprint/util/virtual.py @@ -263,6 +263,8 @@ class VirtualPrinter(): self.outgoing.put("End file list") def _selectSdFile(self, filename): + if filename.startswith("/"): + filename = filename[1:] file = os.path.join(self._virtualSd, filename).lower() if not os.path.exists(file) or not os.path.isfile(file): self.outgoing.put("open failed, File: %s." % filename) @@ -448,6 +450,8 @@ class VirtualPrinter(): pass def _writeSdFile(self, filename): + if filename.startswith("/"): + filename = filename[1:] file = os.path.join(self._virtualSd, filename).lower() if os.path.exists(file): if os.path.isfile(file): @@ -507,6 +511,8 @@ class VirtualPrinter(): time.sleep(delay) def _deleteSdFile(self, filename): + if filename.startswith("/"): + filename = filename[1:] f = os.path.join(self._virtualSd, filename) if os.path.exists(f) and os.path.isfile(f): os.remove(f) diff --git a/tests/printer/test_estimation.py b/tests/printer/test_estimation.py index 28d4ce8a..2944427f 100644 --- a/tests/printer/test_estimation.py +++ b/tests/printer/test_estimation.py @@ -1,5 +1,6 @@ # coding=utf-8 from __future__ import absolute_import +from octoprint.printer.estimation import TimeEstimationHelper __author__ = "Gina Häußge " __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' @@ -15,7 +16,7 @@ import octoprint.printer class EstimationTestCase(unittest.TestCase): def setUp(self): - self.estimation_helper = type(octoprint.printer.TimeEstimationHelper)(octoprint.printer.TimeEstimationHelper.__name__, (octoprint.printer.TimeEstimationHelper,), { + self.estimation_helper = type(TimeEstimationHelper)(TimeEstimationHelper.__name__, (TimeEstimationHelper,), { 'STABLE_THRESHOLD': 0.1, 'STABLE_ROLLING_WINDOW': 3, 'STABLE_COUNTDOWN': 1