2015-03-03 16:01:33 +00:00
|
|
|
# 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
|
2015-03-06 15:04:43 +00:00
|
|
|
from octoprint.printer import PrinterInterface, PrinterCallback, UnknownScript
|
2015-03-03 16:01:33 +00:00
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
#~~ 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):
|
2015-03-30 14:50:06 +00:00
|
|
|
for plugin in self._progressPlugins:
|
2015-03-03 16:01:33 +00:00
|
|
|
try:
|
|
|
|
|
plugin.on_print_progress(storage, filename, progress)
|
|
|
|
|
except:
|
2015-03-30 14:50:06 +00:00
|
|
|
self._logger.exception("Exception while sending print progress to plugin %s" % plugin._identifier)
|
2015-03-03 16:01:33 +00:00
|
|
|
|
|
|
|
|
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`")
|
|
|
|
|
|
2015-04-27 16:13:39 +00:00
|
|
|
def fake_ack(self):
|
|
|
|
|
if self._comm is None:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
self._comm.fakeOk()
|
|
|
|
|
|
2015-03-03 16:01:33 +00:00
|
|
|
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)
|
|
|
|
|
|
2015-03-06 15:04:43 +00:00
|
|
|
def script(self, name, context=None):
|
|
|
|
|
if self._comm is None:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if name is None or not name:
|
|
|
|
|
raise ValueError("name must be set")
|
|
|
|
|
|
|
|
|
|
result = self._comm.sendGcodeScript(name, replacements=context)
|
|
|
|
|
if not result:
|
|
|
|
|
raise UnknownScript(name)
|
|
|
|
|
|
2015-03-03 16:01:33 +00:00
|
|
|
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):
|
2015-03-06 15:54:41 +00:00
|
|
|
if not PrinterInterface.valid_tool_regex.match(tool):
|
|
|
|
|
raise ValueError("tool must match \"tool[0-9]+\": {tool}".format(tool=tool))
|
2015-03-03 16:01:33 +00:00
|
|
|
|
2015-03-06 15:54:41 +00:00
|
|
|
tool_num = int(tool[len("tool"):])
|
|
|
|
|
self.commands("T%d" % tool_num)
|
2015-03-03 16:01:33 +00:00
|
|
|
|
|
|
|
|
def set_temperature(self, heater, value):
|
2015-03-06 15:54:41 +00:00
|
|
|
if not PrinterInterface.valid_heater_regex.match(heater):
|
2015-03-03 16:01:33 +00:00
|
|
|
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")
|
|
|
|
|
|
2015-03-06 15:54:41 +00:00
|
|
|
validated_keys = filter(lambda x: PrinterInterface.valid_heater_regex.match(x), offsets.keys())
|
2015-03-06 00:42:09 +00:00
|
|
|
validated_values = filter(lambda x: isinstance(x, (int, long, float)), offsets.values())
|
2015-03-03 16:01:33 +00:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2015-03-06 00:42:09 +00:00
|
|
|
self._comm.setTemperatureOffset(offsets)
|
|
|
|
|
self._stateMonitor.set_temp_offsets(offsets)
|
2015-03-03 16:01:33 +00:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2015-03-04 19:41:10 +00:00
|
|
|
rolling_window = None
|
|
|
|
|
threshold = None
|
|
|
|
|
countdown = None
|
|
|
|
|
if self._selectedFile["sd"]:
|
|
|
|
|
# we are interesting in a rolling window of roughly the last 15s, so the number of entries has to be derived
|
|
|
|
|
# by that divided by the sd status polling interval
|
|
|
|
|
rolling_window = 15 / settings().get(["serial", "timeout", "sdStatus"])
|
|
|
|
|
|
|
|
|
|
# we are happy if the average of the estimates stays within 60s of the prior one
|
|
|
|
|
threshold = 60
|
|
|
|
|
|
|
|
|
|
# we are happy when one rolling window has been stable
|
|
|
|
|
countdown = rolling_window
|
|
|
|
|
self._timeEstimationData = TimeEstimationHelper(rolling_window=rolling_window, threshold=threshold, countdown=countdown)
|
|
|
|
|
|
2015-03-03 16:01:33 +00:00
|
|
|
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:
|
2015-03-18 14:54:06 +00:00
|
|
|
offsets = self._comm.getOffsets()
|
2015-03-03 16:01:33 +00:00
|
|
|
else:
|
2015-03-18 14:54:06 +00:00
|
|
|
offsets = dict()
|
2015-03-03 16:01:33 +00:00
|
|
|
|
|
|
|
|
result = {}
|
|
|
|
|
if self._temp is not None:
|
|
|
|
|
for tool in self._temp.keys():
|
|
|
|
|
result["tool%d" % tool] = {
|
2015-03-18 14:54:06 +00:00
|
|
|
"actual": self._temp[tool][0],
|
|
|
|
|
"target": self._temp[tool][1],
|
|
|
|
|
"offset": offsets[tool] if tool in offsets and offsets[tool] is not None else 0
|
2015-03-03 16:01:33 +00:00
|
|
|
}
|
|
|
|
|
if self._bedTemp is not None:
|
|
|
|
|
result["bed"] = {
|
2015-03-18 14:54:06 +00:00
|
|
|
"actual": self._bedTemp[0],
|
|
|
|
|
"target": self._bedTemp[1],
|
|
|
|
|
"offset": offsets["bed"] if "bed" in offsets and offsets["bed"] is not None else 0
|
2015-03-03 16:01:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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:
|
2015-03-04 19:39:36 +00:00
|
|
|
path_in_storage = filename
|
2015-03-03 16:01:33 +00:00
|
|
|
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
|
2015-04-22 09:41:52 +00:00
|
|
|
elif state == self._comm.STATE_CLOSED or state == self._comm.STATE_CLOSED_WITH_ERROR:
|
|
|
|
|
if self._comm is not None:
|
|
|
|
|
self._comm = None
|
|
|
|
|
|
|
|
|
|
self._setProgressData(0, None, None, None)
|
|
|
|
|
self._setCurrentZ(None)
|
|
|
|
|
self._setJobData(None, None, None)
|
2015-03-03 16:01:33 +00:00
|
|
|
|
|
|
|
|
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_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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|