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
This commit is contained in:
Gina Häußge 2015-03-03 17:01:33 +01:00
parent af0cad90a8
commit 333c9ba205
16 changed files with 1447 additions and 977 deletions

View file

@ -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"])

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,63 @@
# coding=utf-8
from __future__ import absolute_import
__author__ = "Gina Häußge <osd@foosel.net>"
__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)

View file

@ -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 <osd@foosel.net>"
__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
}

View file

@ -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")

View file

@ -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()

View file

@ -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:

View file

@ -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"],

View file

@ -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():

View file

@ -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": {

View file

@ -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)

View file

@ -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,

View file

@ -2,12 +2,12 @@
<div id="drop_overlay_background"></div>
<div id="drop_overlay_wrapper">
{% if enableSdSupport %}
<div class="dropzone" id="drop_locally"><span class="centered"><i class="icon-upload-alt"></i><br>{{ _('Upload locally') }}</span></div>
<div class="dropzone" id="drop_locally"><span class="text"><i class="icon-upload-alt"></i><br>{{ _('Upload locally') }}</span></div>
<div class="dropzone_background" id="drop_locally_background"></div>
<div class="dropzone" id="drop_sd"><span class="centered"><i class="icon-upload-alt"></i><br>{{ _('Upload to SD') }}<br><small data-bind="visible: !isSdReady()">({{ _('SD not initialized') }})</small></span></div>
<div class="dropzone" id="drop_sd"><span class="text"><i class="icon-upload-alt"></i><br>{{ _('Upload to SD') }}<br><small data-bind="visible: !isSdReady()">({{ _('SD not initialized') }})</small></span></div>
<div class="dropzone_background" id="drop_sd_background"></div>
{% else %}
<div class="dropzone" id="drop"><span class="centered"><i class="icon-upload-alt"></i><br>{{ _('Upload') }}</span></div>
<div class="dropzone" id="drop"><span class="text"><i class="icon-upload-alt"></i><br>{{ _('Upload') }}</span></div>
<div class="dropzone_background" id="drop_background"></div>
{% endif %}
</div>

View file

@ -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 ##################################################################################

View file

@ -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)

View file

@ -1,5 +1,6 @@
# coding=utf-8
from __future__ import absolute_import
from octoprint.printer.estimation import TimeEstimationHelper
__author__ = "Gina Häußge <osd@foosel.net>"
__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