MrDraw/src/octoprint/util/comm.py
Gina Häußge 48d74ef2fd Improved handshake procedure on comm layer
"Hello" command sent to printer to trigger initial handshake can now be
configured. Commands that _always_ necessitate to be sent with checksum/
line number (e.g. M110 on Marlin) can be configured as such too.

Also fixed an issue causing the "Hello" command to not be actually enqueued
first thing on opening a connection. Seems to not have caused harm in the
wild, but was unintentional.
(cherry picked from commit 5c2ae37)
2016-03-09 11:23:50 +01:00

2508 lines
81 KiB
Python

# coding=utf-8
from __future__ import absolute_import
__author__ = "Gina Häußge <osd@foosel.net> based on work by David Braam"
__license__ = "GNU Affero General Public License http://www.gnu.org/licenses/agpl.html"
__copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
import os
import glob
import time
import re
import threading
import Queue as queue
import logging
import serial
import octoprint.plugin
from collections import deque
from octoprint.util.avr_isp import stk500v2
from octoprint.util.avr_isp import ispBase
from octoprint.settings import settings, default_settings
from octoprint.events import eventManager, Events
from octoprint.filemanager import valid_file_type
from octoprint.filemanager.destinations import FileDestinations
from octoprint.util import get_exception_string, sanitize_ascii, filter_non_ascii, CountedEvent, RepeatedTimer, to_unicode, bom_aware_open
try:
import _winreg
except:
pass
_logger = logging.getLogger(__name__)
# a bunch of regexes we'll need for the communication parsing...
regex_float_pattern = "[-+]?[0-9]*\.?[0-9]+"
regex_positive_float_pattern = "[+]?[0-9]*\.?[0-9]+"
regex_int_pattern = "\d+"
regex_command = re.compile("^\s*((?P<commandGM>[GM]\d+)|(?P<commandT>T)\d+)")
"""Regex for a GCODE command."""
regex_float = re.compile(regex_float_pattern)
"""Regex for a float value."""
regexes_parameters = dict(
floatP=re.compile("(^|[^A-Za-z])[Pp](?P<value>%s)" % regex_float_pattern),
floatS=re.compile("(^|[^A-Za-z])[Ss](?P<value>%s)" % regex_float_pattern),
floatZ=re.compile("(^|[^A-Za-z])[Zz](?P<value>%s)" % regex_float_pattern),
intN=re.compile("(^|[^A-Za-z])[Nn](?P<value>%s)" % regex_int_pattern),
intT=re.compile("(^|[^A-Za-z])[Tt](?P<value>%s)" % regex_int_pattern)
)
"""Regexes for parsing various GCODE command parameters."""
regex_minMaxError = re.compile("Error:[0-9]\n")
"""Regex matching first line of min/max errors from the firmware."""
regex_sdPrintingByte = re.compile("(?P<current>[0-9]*)/(?P<total>[0-9]*)")
"""Regex matching SD printing status reports.
Groups will be as follows:
* ``current``: current byte position in file being printed
* ``total``: total size of file being printed
"""
regex_sdFileOpened = re.compile("File opened:\s*(?P<name>.*?)\s+Size:\s*(?P<size>%s)" % regex_int_pattern)
"""Regex matching "File opened" messages from the firmware.
Groups will be as follows:
* ``name``: name of the file reported as having been opened (str)
* ``size``: size of the file in bytes (int)
"""
regex_temp = re.compile("(?P<tool>B|T(?P<toolnum>\d*)):\s*(?P<actual>%s)(\s*\/?\s*(?P<target>%s))?" % (regex_positive_float_pattern, regex_positive_float_pattern))
"""Regex matching temperature entries in line.
Groups will be as follows:
* ``tool``: whole tool designator, incl. optional ``toolnum`` (str)
* ``toolnum``: tool number, if provided (int)
* ``actual``: actual temperature (float)
* ``target``: target temperature, if provided (float)
"""
regex_repetierTempExtr = re.compile("TargetExtr(?P<toolnum>\d+):(?P<target>%s)" % regex_positive_float_pattern)
"""Regex for matching target temp reporting from Repetier.
Groups will be as follows:
* ``toolnum``: number of the extruder to which the target temperature
report belongs (int)
* ``target``: new target temperature (float)
"""
regex_repetierTempBed = re.compile("TargetBed:(?P<target>%s)" % regex_positive_float_pattern)
"""Regex for matching target temp reporting from Repetier for beds.
Groups will be as follows:
* ``target``: new target temperature (float)
"""
def serialList():
baselist=[]
if os.name=="nt":
try:
key=_winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE,"HARDWARE\\DEVICEMAP\\SERIALCOMM")
i=0
while(1):
baselist+=[_winreg.EnumValue(key,i)[1]]
i+=1
except:
pass
baselist = baselist \
+ glob.glob("/dev/ttyUSB*") \
+ glob.glob("/dev/ttyACM*") \
+ glob.glob("/dev/tty.usb*") \
+ glob.glob("/dev/cu.*") \
+ glob.glob("/dev/cuaU*") \
+ glob.glob("/dev/rfcomm*")
additionalPorts = settings().get(["serial", "additionalPorts"])
for additional in additionalPorts:
baselist += glob.glob(additional)
prev = settings().get(["serial", "port"])
if prev in baselist:
baselist.remove(prev)
baselist.insert(0, prev)
if settings().getBoolean(["devel", "virtualPrinter", "enabled"]):
baselist.append("VIRTUAL")
return baselist
def baudrateList():
ret = [250000, 230400, 115200, 57600, 38400, 19200, 9600]
prev = settings().getInt(["serial", "baudrate"])
if prev in ret:
ret.remove(prev)
ret.insert(0, prev)
return ret
gcodeToEvent = {
# pause for user input
"M226": Events.WAITING,
"M0": Events.WAITING,
"M1": Events.WAITING,
# dwell command
"G4": Events.DWELL,
# part cooler
"M245": Events.COOLING,
# part conveyor
"M240": Events.CONVEYOR,
# part ejector
"M40": Events.EJECT,
# user alert
"M300": Events.ALERT,
# home print head
"G28": Events.HOME,
# emergency stop
"M112": Events.E_STOP,
# motors on/off
"M80": Events.POWER_ON,
"M81": Events.POWER_OFF,
}
class MachineCom(object):
STATE_NONE = 0
STATE_OPEN_SERIAL = 1
STATE_DETECT_SERIAL = 2
STATE_DETECT_BAUDRATE = 3
STATE_CONNECTING = 4
STATE_OPERATIONAL = 5
STATE_PRINTING = 6
STATE_PAUSED = 7
STATE_CLOSED = 8
STATE_ERROR = 9
STATE_CLOSED_WITH_ERROR = 10
STATE_TRANSFERING_FILE = 11
def __init__(self, port = None, baudrate=None, callbackObject=None, printerProfileManager=None):
self._logger = logging.getLogger(__name__)
self._serialLogger = logging.getLogger("SERIAL")
if port == None:
port = settings().get(["serial", "port"])
if baudrate == None:
settingsBaudrate = settings().getInt(["serial", "baudrate"])
if settingsBaudrate is None:
baudrate = 0
else:
baudrate = settingsBaudrate
if callbackObject == None:
callbackObject = MachineComPrintCallback()
self._port = port
self._baudrate = baudrate
self._callback = callbackObject
self._printerProfileManager = printerProfileManager
self._state = self.STATE_NONE
self._serial = None
self._baudrateDetectList = baudrateList()
self._baudrateDetectRetry = 0
self._temp = {}
self._bedTemp = None
self._tempOffsets = dict()
self._commandQueue = queue.Queue()
self._currentZ = None
self._heatupWaitStartTime = None
self._heatupWaitTimeLost = 0.0
self._pauseWaitStartTime = None
self._pauseWaitTimeLost = 0.0
self._currentTool = 0
self._formerTool = None
self._long_running_command = False
self._heating = False
self._connection_closing = False
self._timeout = None
self._hello_command = settings().get(["serial", "helloCommand"])
self._alwaysSendChecksum = settings().getBoolean(["feature", "alwaysSendChecksum"])
self._sendChecksumWithUnknownCommands = settings().getBoolean(["feature", "sendChecksumWithUnknownCommands"])
self._unknownCommandsNeedAck = settings().getBoolean(["feature", "unknownCommandsNeedAck"])
self._currentLine = 1
self._line_mutex = threading.RLock()
self._resendDelta = None
self._lastLines = deque([], 50)
self._lastCommError = None
self._lastResendNumber = None
self._currentResendCount = 0
self._resendSwallowNextOk = False
self._resendSwallowRepetitions = settings().getBoolean(["feature", "ignoreIdenticalResends"])
self._resendSwallowRepetitionsCounter = 0
self._disconnect_on_errors = settings().getBoolean(["serial", "disconnectOnErrors"])
self._ignore_errors = settings().getBoolean(["serial", "ignoreErrorsFromFirmware"])
self._long_running_commands = settings().get(["serial", "longRunningCommands"])
self._checksum_requiring_commands = settings().get(["serial", "checksumRequiringCommands"])
self._clear_to_send = CountedEvent(max=10, name="comm.clear_to_send")
self._send_queue = TypedQueue()
self._temperature_timer = None
self._sd_status_timer = None
# hooks
self._pluginManager = octoprint.plugin.plugin_manager()
self._gcode_hooks = dict(
queuing=self._pluginManager.get_hooks("octoprint.comm.protocol.gcode.queuing"),
queued=self._pluginManager.get_hooks("octoprint.comm.protocol.gcode.queued"),
sending=self._pluginManager.get_hooks("octoprint.comm.protocol.gcode.sending"),
sent=self._pluginManager.get_hooks("octoprint.comm.protocol.gcode.sent")
)
self._printer_action_hooks = self._pluginManager.get_hooks("octoprint.comm.protocol.action")
self._gcodescript_hooks = self._pluginManager.get_hooks("octoprint.comm.protocol.scripts")
self._serial_factory_hooks = self._pluginManager.get_hooks("octoprint.comm.transport.serial.factory")
# SD status data
self._sdEnabled = settings().getBoolean(["feature", "sdSupport"])
self._sdAvailable = False
self._sdFileList = False
self._sdFiles = []
self._sdFileToSelect = None
self._ignore_select = False
self._manualStreaming = False
# print job
self._currentFile = None
# multithreading locks
self._sendNextLock = threading.Lock()
self._sendingLock = threading.RLock()
# monitoring thread
self._monitoring_active = True
self.monitoring_thread = threading.Thread(target=self._monitor, name="comm._monitor")
self.monitoring_thread.daemon = True
self.monitoring_thread.start()
# sending thread
self._send_queue_active = True
self.sending_thread = threading.Thread(target=self._send_loop, name="comm.sending_thread")
self.sending_thread.daemon = True
self.sending_thread.start()
def __del__(self):
self.close()
##~~ internal state management
def _changeState(self, newState):
if self._state == newState:
return
if newState == self.STATE_CLOSED or newState == self.STATE_CLOSED_WITH_ERROR:
if settings().get(["feature", "sdSupport"]):
self._sdFileList = False
self._sdFiles = []
self._callback.on_comm_sd_files([])
if self._currentFile is not None:
if self.isBusy():
self._recordFilePosition()
self._currentFile.close()
oldState = self.getStateString()
self._state = newState
self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString()))
self._callback.on_comm_state_change(newState)
def _log(self, message):
self._callback.on_comm_log(message)
self._serialLogger.debug(message)
def _addToLastLines(self, cmd):
self._lastLines.append(cmd)
##~~ getters
def getState(self):
return self._state
def getStateString(self):
if self._state == self.STATE_NONE:
return "Offline"
if self._state == self.STATE_OPEN_SERIAL:
return "Opening serial port"
if self._state == self.STATE_DETECT_SERIAL:
return "Detecting serial port"
if self._state == self.STATE_DETECT_BAUDRATE:
return "Detecting baudrate"
if self._state == self.STATE_CONNECTING:
return "Connecting"
if self._state == self.STATE_OPERATIONAL:
return "Operational"
if self._state == self.STATE_PRINTING:
if self.isSdFileSelected():
return "Printing from SD"
elif self.isStreaming():
return "Sending file to SD"
else:
return "Printing"
if self._state == self.STATE_PAUSED:
return "Paused"
if self._state == self.STATE_CLOSED:
return "Closed"
if self._state == self.STATE_ERROR:
return "Error: %s" % (self.getErrorString())
if self._state == self.STATE_CLOSED_WITH_ERROR:
return "Error: %s" % (self.getErrorString())
if self._state == self.STATE_TRANSFERING_FILE:
return "Transfering file to SD"
return "?%d?" % (self._state)
def getErrorString(self):
return self._errorValue
def isClosedOrError(self):
return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED
def isError(self):
return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR
def isOperational(self):
return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED or self._state == self.STATE_TRANSFERING_FILE
def isPrinting(self):
return self._state == self.STATE_PRINTING
def isSdPrinting(self):
return self.isSdFileSelected() and self.isPrinting()
def isSdFileSelected(self):
return self._currentFile is not None and isinstance(self._currentFile, PrintingSdFileInformation)
def isStreaming(self):
return self._currentFile is not None and isinstance(self._currentFile, StreamingGcodeFileInformation)
def isPaused(self):
return self._state == self.STATE_PAUSED
def isBusy(self):
return self.isPrinting() or self.isPaused()
def isSdReady(self):
return self._sdAvailable
def getPrintProgress(self):
if self._currentFile is None:
return None
return self._currentFile.getProgress()
def getPrintFilepos(self):
if self._currentFile is None:
return None
return self._currentFile.getFilepos()
def getPrintTime(self):
if self._currentFile is None or self._currentFile.getStartTime() is None:
return None
else:
return time.time() - self._currentFile.getStartTime() - self._pauseWaitTimeLost
def getCleanedPrintTime(self):
printTime = self.getPrintTime()
if printTime is None:
return None
cleanedPrintTime = printTime - self._heatupWaitTimeLost
if cleanedPrintTime < 0:
cleanedPrintTime = 0.0
return cleanedPrintTime
def getTemp(self):
return self._temp
def getBedTemp(self):
return self._bedTemp
def getOffsets(self):
return dict(self._tempOffsets)
def getCurrentTool(self):
return self._currentTool
def getConnection(self):
return self._port, self._baudrate
def getTransport(self):
return self._serial
##~~ external interface
def close(self, isError = False):
if self._connection_closing:
return
self._connection_closing = True
if self._temperature_timer is not None:
try:
self._temperature_timer.cancel()
except:
pass
if self._sd_status_timer is not None:
try:
self._sd_status_timer.cancel()
except:
pass
self._monitoring_active = False
self._send_queue_active = False
printing = self.isPrinting() or self.isPaused()
if self._serial is not None:
try:
self._serial.close()
except:
self._logger.exception("Error while trying to close serial port")
isError = True
if isError:
self._changeState(self.STATE_CLOSED_WITH_ERROR)
else:
self._changeState(self.STATE_CLOSED)
self._serial = None
if settings().get(["feature", "sdSupport"]):
self._sdFileList = []
if printing:
payload = None
if self._currentFile is not None:
payload = {
"file": self._currentFile.getFilename(),
"filename": os.path.basename(self._currentFile.getFilename()),
"origin": self._currentFile.getFileLocation()
}
eventManager().fire(Events.PRINT_FAILED, payload)
eventManager().fire(Events.DISCONNECTED)
def setTemperatureOffset(self, offsets):
self._tempOffsets.update(offsets)
def fakeOk(self):
self._clear_to_send.set()
def sendCommand(self, cmd, cmd_type=None, processed=False, force=False):
cmd = to_unicode(cmd, errors="replace")
if not processed:
cmd = process_gcode_line(cmd)
if not cmd:
return
if self.isPrinting() and not self.isSdFileSelected():
self._commandQueue.put((cmd, cmd_type))
elif self.isOperational() or force:
self._sendCommand(cmd, cmd_type=cmd_type)
def sendGcodeScript(self, scriptName, replacements=None):
context = dict()
if replacements is not None and isinstance(replacements, dict):
context.update(replacements)
context.update(dict(
printer_profile=self._printerProfileManager.get_current_or_default()
))
template = settings().loadScript("gcode", scriptName, context=context)
if template is None:
scriptLines = []
else:
scriptLines = filter(
lambda x: x is not None and x.strip() != "",
map(
lambda x: process_gcode_line(x, offsets=self._tempOffsets, current_tool=self._currentTool),
template.split("\n")
)
)
for hook in self._gcodescript_hooks:
try:
retval = self._gcodescript_hooks[hook](self, "gcode", scriptName)
except:
self._logger.exception("Error while processing gcodescript hook %s" % hook)
else:
if retval is None:
continue
if not isinstance(retval, (list, tuple)) or not len(retval) == 2:
continue
def to_list(data):
if isinstance(data, str):
data = map(str.strip, data.split("\n"))
elif isinstance(data, unicode):
data = map(unicode.strip, data.split("\n"))
if isinstance(data, (list, tuple)):
return list(data)
else:
return None
prefix, suffix = map(to_list, retval)
if prefix:
scriptLines = list(prefix) + scriptLines
if suffix:
scriptLines += list(suffix)
for line in scriptLines:
self.sendCommand(line)
return "\n".join(scriptLines)
def startPrint(self, pos=None):
if not self.isOperational() or self.isPrinting():
return
if self._currentFile is None:
raise ValueError("No file selected for printing")
self._heatupWaitStartTime = None
self._heatupWaitTimeLost = 0.0
self._pauseWaitStartTime = 0
self._pauseWaitTimeLost = 0.0
try:
self._currentFile.start()
self._changeState(self.STATE_PRINTING)
self.resetLineNumbers()
payload = {
"file": self._currentFile.getFilename(),
"filename": os.path.basename(self._currentFile.getFilename()),
"origin": self._currentFile.getFileLocation()
}
eventManager().fire(Events.PRINT_STARTED, payload)
self.sendGcodeScript("beforePrintStarted", replacements=dict(event=payload))
if self.isSdFileSelected():
#self.sendCommand("M26 S0") # setting the sd pos apparently sometimes doesn't work, so we re-select
# the file instead
# make sure to ignore the "file selected" later on, otherwise we'll reset our progress data
self._ignore_select = True
self.sendCommand("M23 {filename}".format(filename=self._currentFile.getFilename()))
if pos is not None and isinstance(pos, int) and pos > 0:
self._currentFile.setFilepos(pos)
self.sendCommand("M26 S{}".format(pos))
else:
self._currentFile.setFilepos(0)
self.sendCommand("M24")
self._sd_status_timer = RepeatedTimer(lambda: get_interval("sdStatus", default_value=1.0), self._poll_sd_status, run_first=True)
self._sd_status_timer.start()
else:
if pos is not None and isinstance(pos, int) and pos > 0:
self._currentFile.seek(pos)
line = self._getNext()
if line is not None:
self.sendCommand(line)
# now make sure we actually do something, up until now we only filled up the queue
self._sendFromQueue()
except:
self._logger.exception("Error while trying to start printing")
self._errorValue = get_exception_string()
self._changeState(self.STATE_ERROR)
eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
def startFileTransfer(self, filename, localFilename, remoteFilename):
if not self.isOperational() or self.isBusy():
logging.info("Printer is not operation or busy")
return
self._currentFile = StreamingGcodeFileInformation(filename, localFilename, remoteFilename)
self._currentFile.start()
self.sendCommand("M28 %s" % remoteFilename)
eventManager().fire(Events.TRANSFER_STARTED, {"local": localFilename, "remote": remoteFilename})
self._callback.on_comm_file_transfer_started(remoteFilename, self._currentFile.getFilesize())
def selectFile(self, filename, sd):
if self.isBusy():
return
if sd:
if not self.isOperational():
# printer is not connected, can't use SD
return
self._sdFileToSelect = filename
self.sendCommand("M23 %s" % filename)
else:
self._currentFile = PrintingGcodeFileInformation(filename, offsets_callback=self.getOffsets, current_tool_callback=self.getCurrentTool)
eventManager().fire(Events.FILE_SELECTED, {
"file": self._currentFile.getFilename(),
"filename": os.path.basename(self._currentFile.getFilename()),
"origin": self._currentFile.getFileLocation()
})
self._callback.on_comm_file_selected(filename, self._currentFile.getFilesize(), False)
def unselectFile(self):
if self.isBusy():
return
self._currentFile = None
eventManager().fire(Events.FILE_DESELECTED)
self._callback.on_comm_file_selected(None, None, False)
def cancelPrint(self, firmware_error=None):
if not self.isOperational() or self.isStreaming():
return
if not self.isBusy() or self._currentFile is None:
# we aren't even printing, nothing to cancel...
return
self._changeState(self.STATE_OPERATIONAL)
if self.isSdFileSelected():
self.sendCommand("M25") # pause print
self.sendCommand("M26 S0") # reset position in file to byte 0
if self._sd_status_timer is not None:
try:
self._sd_status_timer.cancel()
except:
pass
self._recordFilePosition()
payload = {
"file": self._currentFile.getFilename(),
"filename": os.path.basename(self._currentFile.getFilename()),
"origin": self._currentFile.getFileLocation(),
"firmwareError": firmware_error
}
self.sendGcodeScript("afterPrintCancelled", replacements=dict(event=payload))
eventManager().fire(Events.PRINT_CANCELLED, payload)
def setPause(self, pause):
if self.isStreaming():
return
if not self._currentFile:
return
payload = {
"file": self._currentFile.getFilename(),
"filename": os.path.basename(self._currentFile.getFilename()),
"origin": self._currentFile.getFileLocation()
}
if not pause and self.isPaused():
if self._pauseWaitStartTime:
self._pauseWaitTimeLost = self._pauseWaitTimeLost + (time.time() - self._pauseWaitStartTime)
self._pauseWaitStartTime = None
self._changeState(self.STATE_PRINTING)
self.sendGcodeScript("beforePrintResumed", replacements=dict(event=payload))
if self.isSdFileSelected():
self.sendCommand("M24")
self.sendCommand("M27")
else:
line = self._getNext()
if line is not None:
self.sendCommand(line)
# now make sure we actually do something, up until now we only filled up the queue
self._sendFromQueue()
eventManager().fire(Events.PRINT_RESUMED, payload)
elif pause and self.isPrinting():
if not self._pauseWaitStartTime:
self._pauseWaitStartTime = time.time()
self._changeState(self.STATE_PAUSED)
if self.isSdFileSelected():
self.sendCommand("M25") # pause print
self.sendGcodeScript("afterPrintPaused", replacements=dict(event=payload))
eventManager().fire(Events.PRINT_PAUSED, payload)
def getSdFiles(self):
return self._sdFiles
def startSdFileTransfer(self, filename):
if not self._sdEnabled:
return
if not self.isOperational() or self.isBusy():
return
self._changeState(self.STATE_TRANSFERING_FILE)
self.sendCommand("M28 %s" % filename.lower())
def endSdFileTransfer(self, filename):
if not self._sdEnabled:
return
if not self.isOperational() or self.isBusy():
return
self.sendCommand("M29 %s" % filename.lower())
self._changeState(self.STATE_OPERATIONAL)
self.refreshSdFiles()
def deleteSdFile(self, filename):
if not self._sdEnabled:
return
if not self.isOperational() or (self.isBusy() and
isinstance(self._currentFile, PrintingSdFileInformation) and
self._currentFile.getFilename() == filename):
# do not delete a file from sd we are currently printing from
return
self.sendCommand("M30 %s" % filename.lower())
self.refreshSdFiles()
def refreshSdFiles(self):
if not self._sdEnabled:
return
if not self.isOperational() or self.isBusy():
return
self.sendCommand("M20")
def initSdCard(self):
if not self._sdEnabled:
return
if not self.isOperational():
return
self.sendCommand("M21")
if settings().getBoolean(["feature", "sdAlwaysAvailable"]):
self._sdAvailable = True
self.refreshSdFiles()
self._callback.on_comm_sd_state_change(self._sdAvailable)
def releaseSdCard(self):
if not self._sdEnabled:
return
if not self.isOperational() or (self.isBusy() and self.isSdFileSelected()):
# do not release the sd card if we are currently printing from it
return
self.sendCommand("M22")
self._sdAvailable = False
self._sdFiles = []
self._callback.on_comm_sd_state_change(self._sdAvailable)
self._callback.on_comm_sd_files(self._sdFiles)
def sayHello(self):
self.sendCommand(self._hello_command, force=True)
self._clear_to_send.set()
def resetLineNumbers(self, number=0):
if not self.isOperational():
return
self.sendCommand("M110 N%d" % number)
##~~ record aborted file positions
def _recordFilePosition(self):
if self._currentFile is None:
return
origin = self._currentFile.getFileLocation()
filename = self._currentFile.getFilename()
pos = self._currentFile.getFilepos()
self._callback.on_comm_record_fileposition(origin, filename, pos)
##~~ communication monitoring and handling
def _processTemperatures(self, line):
current_tool = self._currentTool if self._currentTool is not None else 0
maxToolNum, parsedTemps = parse_temperature_line(line, current_tool)
if "T0" in parsedTemps.keys():
for n in range(maxToolNum + 1):
tool = "T%d" % n
if not tool in parsedTemps.keys():
continue
actual, target = parsedTemps[tool]
if target is not None:
self._temp[n] = (actual, target)
elif n in self._temp and self._temp[n] is not None and isinstance(self._temp[n], tuple):
(oldActual, oldTarget) = self._temp[n]
self._temp[n] = (actual, oldTarget)
else:
self._temp[n] = (actual, None)
# bed temperature
if "B" in parsedTemps.keys():
actual, target = parsedTemps["B"]
if target is not None:
self._bedTemp = (actual, target)
elif self._bedTemp is not None and isinstance(self._bedTemp, tuple):
(oldActual, oldTarget) = self._bedTemp
self._bedTemp = (actual, oldTarget)
else:
self._bedTemp = (actual, None)
##~~ Serial monitor processing received messages
def _monitor(self):
feedback_controls, feedback_matcher = convert_feedback_controls(settings().get(["controls"]))
feedback_errors = []
pause_triggers = convert_pause_triggers(settings().get(["printerParameters", "pauseTriggers"]))
disable_external_heatup_detection = not settings().getBoolean(["feature", "externalHeatupDetection"])
#Open the serial port.
if not self._openSerial():
return
try_hello = not settings().getBoolean(["feature", "waitForStartOnConnect"])
self._log("Connected to: %s, starting monitor" % self._serial)
if self._baudrate == 0:
self._serial.timeout = 0.01
try_hello = False
self._log("Starting baud rate detection")
self._changeState(self.STATE_DETECT_BAUDRATE)
else:
self._changeState(self.STATE_CONNECTING)
#Start monitoring the serial port.
self._timeout = get_new_timeout("communication")
startSeen = False
supportRepetierTargetTemp = settings().getBoolean(["feature", "repetierTargetTemp"])
supportWait = settings().getBoolean(["feature", "supportWait"])
connection_timeout = settings().getFloat(["serial", "timeout", "connection"])
detection_timeout = settings().getFloat(["serial", "timeout", "detection"])
# enqueue the "hello command" first thing
if try_hello:
self.sayHello()
while self._monitoring_active:
try:
line = self._readline()
if line is None:
break
if line.strip() is not "":
self._timeout = get_new_timeout("communication")
##~~ debugging output handling
if line.startswith("//"):
debugging_output = line[2:].strip()
if debugging_output.startswith("action:"):
action_command = debugging_output[len("action:"):].strip()
if action_command == "pause":
self._log("Pausing on request of the printer...")
self.setPause(True)
elif action_command == "resume":
self._log("Resuming on request of the printer...")
self.setPause(False)
elif action_command == "disconnect":
self._log("Disconnecting on request of the printer...")
self._callback.on_comm_force_disconnect()
else:
for hook in self._printer_action_hooks:
try:
self._printer_action_hooks[hook](self, line, action_command)
except:
self._logger.exception("Error while calling hook {} with action command {}".format(self._printer_action_hooks[hook], action_command))
continue
else:
continue
def convert_line(line):
if line is None:
return None, None
stripped_line = line.strip()
return stripped_line, stripped_line.lower()
##~~ Error handling
line = self._handleErrors(line)
line, lower_line = convert_line(line)
##~~ SD file list
# if we are currently receiving an sd file list, each line is just a filename, so just read it and abort processing
if self._sdFileList and not "End file list" in line:
preprocessed_line = lower_line
fileinfo = preprocessed_line.rsplit(None, 1)
if len(fileinfo) > 1:
# we might have extended file information here, so let's split filename and size and try to make them a bit nicer
filename, size = fileinfo
try:
size = int(size)
except ValueError:
# whatever that was, it was not an integer, so we'll just use the whole line as filename and set size to None
filename = preprocessed_line
size = None
else:
# no extended file information, so only the filename is there and we set size to None
filename = preprocessed_line
size = None
if valid_file_type(filename, "machinecode"):
if filter_non_ascii(filename):
self._logger.warn("Got a file from printer's SD that has a non-ascii filename (%s), that shouldn't happen according to the protocol" % filename)
else:
if not filename.startswith("/"):
# file from the root of the sd -- we'll prepend a /
filename = "/" + filename
self._sdFiles.append((filename, size))
continue
##~~ process oks
if line.strip().startswith("ok") or (self.isPrinting() and supportWait and line.strip().startswith("wait")):
self._clear_to_send.set()
self._long_running_command = False
##~~ Temperature processing
if ' T:' in line or line.startswith('T:') or ' T0:' in line or line.startswith('T0:') or ' B:' in line or line.startswith('B:'):
if not disable_external_heatup_detection and not line.strip().startswith("ok") and not self._heating:
self._logger.debug("Externally triggered heatup detected")
self._heating = True
self._heatupWaitStartTime = time.time()
self._processTemperatures(line)
self._callback.on_comm_temperature_update(self._temp, self._bedTemp)
elif supportRepetierTargetTemp and ('TargetExtr' in line or 'TargetBed' in line):
matchExtr = regex_repetierTempExtr.match(line)
matchBed = regex_repetierTempBed.match(line)
if matchExtr is not None:
toolNum = int(matchExtr.group(1))
try:
target = float(matchExtr.group(2))
if toolNum in self._temp.keys() and self._temp[toolNum] is not None and isinstance(self._temp[toolNum], tuple):
(actual, oldTarget) = self._temp[toolNum]
self._temp[toolNum] = (actual, target)
else:
self._temp[toolNum] = (None, target)
self._callback.on_comm_temperature_update(self._temp, self._bedTemp)
except ValueError:
pass
elif matchBed is not None:
try:
target = float(matchBed.group(1))
if self._bedTemp is not None and isinstance(self._bedTemp, tuple):
(actual, oldTarget) = self._bedTemp
self._bedTemp = (actual, target)
else:
self._bedTemp = (None, target)
self._callback.on_comm_temperature_update(self._temp, self._bedTemp)
except ValueError:
pass
#If we are waiting for an M109 or M190 then measure the time we lost during heatup, so we can remove that time from our printing time estimate.
if line.startswith("ok"):
if self._formerTool is not None:
self._currentTool = self._formerTool
self._formerTool = None
if self._heatupWaitStartTime:
self._heatupWaitTimeLost = self._heatupWaitTimeLost + (time.time() - self._heatupWaitStartTime)
self._heatupWaitStartTime = None
self._heating = False
##~~ SD Card handling
elif 'SD init fail' in line or 'volume.init failed' in line or 'openRoot failed' in line:
self._sdAvailable = False
self._sdFiles = []
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...
self._sdFilePos = 0
self._changeState(self.STATE_OPERATIONAL)
elif 'SD card ok' in line and not self._sdAvailable:
self._sdAvailable = True
self.refreshSdFiles()
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.on_comm_sd_files(self._sdFiles)
elif 'SD printing byte' in line and self.isSdPrinting():
# answer to M27, at least on Marlin, Repetier and Sprinter: "SD printing byte %d/%d"
match = regex_sdPrintingByte.search(line)
self._currentFile.setFilepos(int(match.group("current")))
self._callback.on_comm_progress()
elif 'File opened' in line and not self._ignore_select:
# answer to M23, at least on Marlin, Repetier and Sprinter: "File opened:%s Size:%d"
match = regex_sdFileOpened.search(line)
if self._sdFileToSelect:
name = self._sdFileToSelect
self._sdFileToSelect = None
else:
name = match.group("name")
self._currentFile = PrintingSdFileInformation(name, int(match.group("size")))
elif 'File selected' in line:
if self._ignore_select:
self._ignore_select = False
elif self._currentFile is not None and self.isSdFileSelected():
# final answer to M23, at least on Marlin, Repetier and Sprinter: "File selected"
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()
})
elif 'Writing to file' in line and self.isStreaming():
self._changeState(self.STATE_PRINTING)
self._clear_to_send.set()
line, lower_line = convert_line("ok")
elif 'Done printing file' in line and self.isSdPrinting():
# printer is reporting file finished printing
self._sdFilePos = 0
self._callback.on_comm_print_job_done()
self._changeState(self.STATE_OPERATIONAL)
eventManager().fire(Events.PRINT_DONE, {
"file": self._currentFile.getFilename(),
"filename": os.path.basename(self._currentFile.getFilename()),
"origin": self._currentFile.getFileLocation(),
"time": self.getPrintTime()
})
if self._sd_status_timer is not None:
try:
self._sd_status_timer.cancel()
except:
pass
elif 'Done saving file' in line:
self.refreshSdFiles()
elif 'File deleted' in line and line.strip().endswith("ok"):
# buggy Marlin version that doesn't send a proper \r after the "File deleted" statement, fixed in
# current versions
self._clear_to_send.set()
##~~ Message handling
elif line != '' \
and not line.startswith("ok") \
and not line.startswith("wait") \
and not line.startswith('Resend:') \
and line != 'echo:Unknown command:""\n' \
and self.isOperational():
self._callback.on_comm_message(line)
##~~ Parsing for feedback commands
if feedback_controls and feedback_matcher and not "_all" in feedback_errors:
try:
self._process_registered_message(line, feedback_matcher, feedback_controls, feedback_errors)
except:
# something went wrong while feedback matching
self._logger.exception("Error while trying to apply feedback control matching, disabling it")
feedback_errors.append("_all")
##~~ Parsing for pause triggers
if pause_triggers and not self.isStreaming():
if "enable" in pause_triggers.keys() and pause_triggers["enable"].search(line) is not None:
self.setPause(True)
elif "disable" in pause_triggers.keys() and pause_triggers["disable"].search(line) is not None:
self.setPause(False)
elif "toggle" in pause_triggers.keys() and pause_triggers["toggle"].search(line) is not None:
self.setPause(not self.isPaused())
### Baudrate detection
if self._state == self.STATE_DETECT_BAUDRATE:
if line == '' or time.time() > self._timeout:
if self._baudrateDetectRetry > 0:
self._serial.timeout = detection_timeout
self._baudrateDetectRetry -= 1
self._serial.write('\n')
self._log("Baudrate test retry: %d" % (self._baudrateDetectRetry))
self.sayHello()
elif len(self._baudrateDetectList) > 0:
baudrate = self._baudrateDetectList.pop(0)
try:
self._serial.baudrate = baudrate
if self._serial.timeout != connection_timeout:
self._serial.timeout = connection_timeout
self._log("Trying baudrate: %d" % (baudrate))
self._baudrateDetectRetry = 5
self._timeout = get_new_timeout("communication")
self._serial.write('\n')
self.sayHello()
except:
self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, get_exception_string()))
else:
self.close()
self._errorValue = "No more baudrates to test, and no suitable baudrate found."
self._changeState(self.STATE_ERROR)
eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
elif 'start' in line or 'ok' in line:
self._onConnected()
self._clear_to_send.set()
### Connection attempt
elif self._state == self.STATE_CONNECTING:
if "start" in line and not startSeen:
startSeen = True
self.sayHello()
elif line.startswith("ok"):
self._onConnected()
elif time.time() > self._timeout:
self.close()
### Operational
elif self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PAUSED:
if line.startswith("ok"):
self._handle_ok()
# resend -> start resend procedure from requested line
elif lower_line.startswith("resend") or lower_line.startswith("rs"):
self._handleResendRequest(line)
### Printing
elif self._state == self.STATE_PRINTING:
if line == "" and time.time() > self._timeout:
if not self._long_running_command:
self._log("Communication timeout during printing, forcing a line")
self._sendCommand("M105")
self._clear_to_send.set()
else:
self._logger.debug("Ran into a communication timeout, but a command known to be a long runner is currently active")
if line.startswith("ok") or (supportWait and line.startswith("wait")):
# a wait while printing means our printer's buffer ran out, probably due to some ok getting
# swallowed, so we treat it the same as an ok here to take up communication again
self._handle_ok()
elif lower_line.startswith("resend") or lower_line.startswith("rs"):
self._handleResendRequest(line)
except:
self._logger.exception("Something crashed inside the serial connection loop, please report this in OctoPrint's bug tracker:")
errorMsg = "See octoprint.log for details"
self._log(errorMsg)
self._errorValue = errorMsg
self._changeState(self.STATE_ERROR)
eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
self._log("Connection closed, closing down monitor")
def _handle_ok(self):
if not self._state in (self.STATE_PRINTING, self.STATE_OPERATIONAL, self.STATE_PAUSED):
return
if self._resendSwallowNextOk:
self._resendSwallowNextOk = False
elif self._resendDelta is not None:
self._resendNextCommand()
else:
self._continue_sending()
def _continue_sending(self):
if self._state == self.STATE_PRINTING:
if not self._sendFromQueue() and not self.isSdPrinting():
self._sendNext()
elif self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PAUSED:
self._sendFromQueue()
def _process_registered_message(self, line, feedback_matcher, feedback_controls, feedback_errors):
feedback_match = feedback_matcher.search(line)
if feedback_match is None:
return
for match_key in feedback_match.groupdict():
try:
feedback_key = match_key[len("group"):]
if not feedback_key in feedback_controls or feedback_key in feedback_errors or feedback_match.group(match_key) is None:
continue
matched_part = feedback_match.group(match_key)
if feedback_controls[feedback_key]["matcher"] is None:
continue
match = feedback_controls[feedback_key]["matcher"].search(matched_part)
if match is None:
continue
outputs = dict()
for template_key, template in feedback_controls[feedback_key]["templates"].items():
try:
output = template.format(*match.groups())
except KeyError:
output = template.format(**match.groupdict())
except:
output = None
if output is not None:
outputs[template_key] = output
eventManager().fire(Events.REGISTERED_MESSAGE_RECEIVED, dict(key=feedback_key, matched=matched_part, outputs=outputs))
except:
self._logger.exception("Error while trying to match feedback control output, disabling key {key}".format(key=match_key))
feedback_errors.append(match_key)
def _poll_temperature(self):
"""
Polls the temperature after the temperature timeout, re-enqueues itself.
If the printer is not operational, not printing from sd, busy with a long running command or heating, no poll
will be done.
"""
if self.isOperational() and not self.isStreaming() and not self._long_running_command and not self._heating and not self._manualStreaming:
self.sendCommand("M105", cmd_type="temperature_poll")
def _poll_sd_status(self):
"""
Polls the sd printing status after the sd status timeout, re-enqueues itself.
If the printer is not operational, not printing from sd, busy with a long running command or heating, no poll
will be done.
"""
if self.isOperational() and self.isSdPrinting() and not self._long_running_command and not self._heating:
self.sendCommand("M27", cmd_type="sd_status_poll")
def _onConnected(self):
self._serial.timeout = settings().getFloat(["serial", "timeout", "communication"])
self._temperature_timer = RepeatedTimer(lambda: get_interval("temperature", default_value=4.0), self._poll_temperature, run_first=True)
self._temperature_timer.start()
self._changeState(self.STATE_OPERATIONAL)
self.resetLineNumbers()
if self._sdAvailable:
self.refreshSdFiles()
else:
self.initSdCard()
payload = dict(port=self._port, baudrate=self._baudrate)
eventManager().fire(Events.CONNECTED, payload)
self.sendGcodeScript("afterPrinterConnected", replacements=dict(event=payload))
def _sendFromQueue(self):
# We loop here to make sure that if we do NOT send the first command
# from the queue, we'll send the second (if there is one). We do not
# want to get stuck here by throwing away commands.
while True:
if self._commandQueue.empty() or self.isStreaming():
# no command queue or irrelevant command queue => return
return False
entry = self._commandQueue.get()
if isinstance(entry, tuple):
if not len(entry) == 2:
# something with that entry is broken, ignore it and fetch
# the next one
continue
cmd, cmd_type = entry
else:
cmd = entry
cmd_type = None
if self._sendCommand(cmd, cmd_type=cmd_type):
# we actually did add this cmd to the send queue, so let's
# return, we are done here
return True
def _detectPort(self, close):
programmer = stk500v2.Stk500v2()
self._log("Serial port list: %s" % (str(serialList())))
for p in serialList():
serial_obj = None
try:
self._log("Connecting to: %s" % (p))
programmer.connect(p)
serial_obj = programmer.leaveISP()
except ispBase.IspError as (e):
self._log("Error while connecting to %s: %s" % (p, str(e)))
except:
self._log("Unexpected error while connecting to serial port: %s %s" % (p, get_exception_string()))
if serial_obj is not None:
if (close):
serial_obj.close()
return serial_obj
programmer.close()
return None
def _openSerial(self):
def default(_, port, baudrate, read_timeout):
if port is None or port == 'AUTO':
# no known port, try auto detection
self._changeState(self.STATE_DETECT_SERIAL)
serial_obj = self._detectPort(True)
if serial_obj is None:
self._errorValue = 'Failed to autodetect serial port, please set it manually.'
self._changeState(self.STATE_ERROR)
eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
self._log("Failed to autodetect serial port, please set it manually.")
return None
port = serial_obj.port
# connect to regular serial port
self._log("Connecting to: %s" % port)
if baudrate == 0:
baudrates = baudrateList()
serial_obj = serial.Serial(str(port), 115200 if 115200 in baudrates else baudrates[0], timeout=read_timeout, writeTimeout=10000, parity=serial.PARITY_ODD)
else:
serial_obj = serial.Serial(str(port), baudrate, timeout=read_timeout, writeTimeout=10000, parity=serial.PARITY_ODD)
serial_obj.close()
serial_obj.parity = serial.PARITY_NONE
serial_obj.open()
return serial_obj
serial_factories = self._serial_factory_hooks.items() + [("default", default)]
for name, factory in serial_factories:
try:
serial_obj = factory(self, self._port, self._baudrate, settings().getFloat(["serial", "timeout", "connection"]))
except:
exception_string = get_exception_string()
self._errorValue = "Connection error, see Terminal tab"
self._changeState(self.STATE_ERROR)
eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
self._log("Unexpected error while connecting to serial port: %s %s (hook %s)" % (self._port, exception_string, name))
if "failed to set custom baud rate" in exception_string.lower():
self._log("Your installation does not support custom baudrates (e.g. 250000) for connecting to your printer. This is a problem of the pyserial library that OctoPrint depends on. Please update to a pyserial version that supports your baudrate or switch your printer's firmware to a standard baudrate (e.g. 115200). See https://github.com/foosel/OctoPrint/wiki/OctoPrint-support-for-250000-baud-rate-on-Raspbian")
return False
if serial_obj is not None:
# first hook to succeed wins, but any can pass on to the next
self._changeState(self.STATE_OPEN_SERIAL)
self._serial = serial_obj
self._clear_to_send.clear()
return True
return False
def _handleErrors(self, line):
if line is None:
return
lower_line = line.lower()
# No matter the state, if we see an error, goto the error state and store the error for reference.
if line.startswith('Error:') or line.startswith('!!'):
#Oh YEAH, consistency.
# Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n"
# But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!"
# So we can have an extra newline in the most common case. Awesome work people.
if regex_minMaxError.match(line):
line = line.rstrip() + self._readline()
if 'line number' in lower_line or 'checksum' in lower_line or 'format error' in lower_line or 'expected line' in lower_line:
#Skip the communication errors, as those get corrected.
self._lastCommError = line[6:] if line.startswith("Error:") else line[2:]
pass
elif 'volume.init' in lower_line or "openroot" in lower_line or 'workdir' in lower_line\
or "error writing to file" in lower_line or "cannot open" in lower_line\
or "cannot enter" in lower_line:
#Also skip errors with the SD card
pass
elif 'unknown command' in lower_line:
#Ignore unkown command errors, it could be a typo or some missing feature
pass
elif not self.isError():
error_text = line[6:] if line.startswith("Error:") else line[2:]
if not self._ignore_errors:
if self._disconnect_on_errors:
self._errorValue = error_text
self._changeState(self.STATE_ERROR)
eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
elif self.isPrinting():
self.cancelPrint(firmware_error=error_text)
self._clear_to_send.set()
else:
self._log("WARNING! Received an error from the printer's firmware, ignoring that as configured but you might want to investigate what happened here! Error: {}".format(error_text))
self._clear_to_send.set()
return line
def _readline(self):
if self._serial is None:
return None
try:
ret = self._serial.readline()
except:
if not self._connection_closing:
self._logger.exception("Unexpected error while reading from serial port")
self._log("Unexpected error while reading serial port, please consult octoprint.log for details: %s" % (get_exception_string()))
self._errorValue = get_exception_string()
self.close(True)
return None
if ret == '':
#self._log("Recv: TIMEOUT")
return ''
try:
self._log("Recv: %s" % sanitize_ascii(ret))
except ValueError as e:
self._log("WARN: While reading last line: %s" % e)
self._log("Recv: %r" % ret)
return ret
def _getNext(self):
if self._currentFile is None:
return None
line = self._currentFile.getNext()
if line is None:
if self.isStreaming():
self._sendCommand("M29")
remote = self._currentFile.getRemoteFilename()
payload = {
"local": self._currentFile.getLocalFilename(),
"remote": remote,
"time": self.getPrintTime()
}
self._currentFile = None
self._changeState(self.STATE_OPERATIONAL)
self._callback.on_comm_file_transfer_done(remote)
eventManager().fire(Events.TRANSFER_DONE, payload)
self.refreshSdFiles()
else:
payload = {
"file": self._currentFile.getFilename(),
"filename": os.path.basename(self._currentFile.getFilename()),
"origin": self._currentFile.getFileLocation(),
"time": self.getPrintTime()
}
self._callback.on_comm_print_job_done()
self._changeState(self.STATE_OPERATIONAL)
eventManager().fire(Events.PRINT_DONE, payload)
self.sendGcodeScript("afterPrintDone", replacements=dict(event=payload))
return line
def _sendNext(self):
with self._sendNextLock:
line = self._getNext()
if line is not None:
self._sendCommand(line)
self._callback.on_comm_progress()
def _handleResendRequest(self, line):
lineToResend = None
try:
lineToResend = int(line.replace("N:", " ").replace("N", " ").replace(":", " ").split()[-1])
except:
if "rs" in line:
lineToResend = int(line.split()[1])
if lineToResend is not None:
self._resendSwallowNextOk = True
lastCommError = self._lastCommError
self._lastCommError = None
resendDelta = self._currentLine - lineToResend
if lastCommError is not None \
and ("line number" in lastCommError.lower() or "expected line" in lastCommError.lower()) \
and lineToResend == self._lastResendNumber \
and self._resendDelta is not None and self._currentResendCount < self._resendDelta:
self._logger.debug("Ignoring resend request for line %d, that still originates from lines we sent before we got the first resend request" % lineToResend)
self._currentResendCount += 1
return
# If we ignore resend repetitions (Repetier firmware...), check if we
# need to do this now. If the same line number has been requested we
# already saw and resent, we'll ignore it up to <counter> times.
if self._resendSwallowRepetitions and lineToResend == self._lastResendNumber and self._resendSwallowRepetitionsCounter > 0:
self._logger.debug("Ignoring resend request for line %d, that is probably a repetition sent by the firmware to ensure it arrives, not a real request" % lineToResend)
self._resendSwallowRepetitionsCounter -= 1
return
self._resendDelta = resendDelta
self._lastResendNumber = lineToResend
self._currentResendCount = 0
self._resendSwallowRepetitionsCounter = settings().getInt(["feature", "identicalResendsCountdown"])
if self._resendDelta > len(self._lastLines) or len(self._lastLines) == 0 or self._resendDelta < 0:
self._errorValue = "Printer requested line %d but no sufficient history is available, can't resend" % lineToResend
self._logger.warn(self._errorValue)
if self.isPrinting():
# abort the print, there's nothing we can do to rescue it now
self._changeState(self.STATE_ERROR)
eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
else:
# reset resend delta, we can't do anything about it
self._resendDelta = None
else:
self._resendNextCommand()
def _resendNextCommand(self):
self._lastCommError = None
# Make sure we are only handling one sending job at a time
with self._sendingLock:
cmd = self._lastLines[-self._resendDelta]
lineNumber = self._currentLine - self._resendDelta
self._enqueue_for_sending(cmd, linenumber=lineNumber)
self._resendDelta -= 1
if self._resendDelta <= 0:
self._resendDelta = None
self._lastResendNumber = None
self._currentResendCount = 0
def _sendCommand(self, cmd, cmd_type=None):
# Make sure we are only handling one sending job at a time
with self._sendingLock:
if self._serial is None:
return False
gcode = None
if not self.isStreaming():
# trigger the "queuing" phase only if we are not streaming to sd right now
cmd, cmd_type, gcode = self._process_command_phase("queuing", cmd, cmd_type, gcode=gcode)
if cmd is None:
# command is no more, return
return False
if gcode and gcode in gcodeToEvent:
# if this is a gcode bound to an event, trigger that now
eventManager().fire(gcodeToEvent[gcode])
# actually enqueue the command for sending
self._enqueue_for_sending(cmd, command_type=cmd_type)
if not self.isStreaming():
# trigger the "queued" phase only if we are not streaming to sd right now
self._process_command_phase("queued", cmd, cmd_type, gcode=gcode)
return True
##~~ send loop handling
def _enqueue_for_sending(self, command, linenumber=None, command_type=None):
"""
Enqueues a command an optional linenumber to use for it in the send queue.
Arguments:
command (str): The command to send.
linenumber (int): The line number with which to send the command. May be ``None`` in which case the command
will be sent without a line number and checksum.
"""
try:
self._send_queue.put((command, linenumber, command_type))
except TypeAlreadyInQueue as e:
self._logger.debug("Type already in queue: " + e.type)
def _send_loop(self):
"""
The send loop is reponsible of sending commands in ``self._send_queue`` over the line, if it is cleared for
sending (through received ``ok`` responses from the printer's firmware.
"""
self._clear_to_send.wait()
while self._send_queue_active:
try:
# wait until we have something in the queue
entry = self._send_queue.get()
# make sure we are still active
if not self._send_queue_active:
break
# fetch command and optional linenumber from queue
command, linenumber, command_type = entry
# some firmwares (e.g. Smoothie) might support additional in-band communication that will not
# stick to the acknowledgement behaviour of GCODE, so we check here if we have a GCODE command
# at hand here and only clear our clear_to_send flag later if that's the case
gcode = gcode_command_for_cmd(command)
if linenumber is not None:
# line number predetermined - this only happens for resends, so we'll use the number and
# send directly without any processing (since that already took place on the first sending!)
self._doSendWithChecksum(command, linenumber)
else:
# trigger "sending" phase
command, _, gcode = self._process_command_phase("sending", command, command_type, gcode=gcode)
if command is None:
# No, we are not going to send this, that was a last-minute bail.
# However, since we already are in the send queue, our _monitor
# loop won't be triggered with the reply from this unsent command
# now, so we try to tickle the processing of any active
# command queues manually
self._continue_sending()
# and now let's fetch the next item from the queue
continue
# now comes the part where we increase line numbers and send stuff - no turning back now
command_requiring_checksum = gcode is not None and gcode in self._checksum_requiring_commands
command_allowing_checksum = gcode is not None or self._sendChecksumWithUnknownCommands
checksum_enabled = self.isPrinting() or self._alwaysSendChecksum
command_to_send = command.encode("ascii", errors="replace")
if command_requiring_checksum or (command_allowing_checksum and checksum_enabled):
self._doIncrementAndSendWithChecksum(command_to_send)
else:
self._doSendWithoutChecksum(command_to_send)
# trigger "sent" phase and use up one "ok"
self._process_command_phase("sent", command, command_type, gcode=gcode)
# we only need to use up a clear if the command we just sent was either a gcode command or if we also
# require ack's for unknown commands
use_up_clear = self._unknownCommandsNeedAck
if gcode is not None:
use_up_clear = True
if use_up_clear:
# if we need to use up a clear, do that now
self._clear_to_send.clear()
else:
# Otherwise we need to tickle the read queue - there might not be a reply
# to this command, so our _monitor loop will stay waiting until timeout. We
# definitely do not want that, so we tickle the queue manually here
self._continue_sending()
# now we just wait for the next clear and then start again
self._clear_to_send.wait()
except:
self._logger.exception("Caught an exception in the send loop")
self._log("Closing down send loop")
def _process_command_phase(self, phase, command, command_type=None, gcode=None):
if phase not in ("queuing", "queued", "sending", "sent"):
return command, command_type, gcode
if gcode is None:
gcode = gcode_command_for_cmd(command)
# send it through the phase specific handlers provided by plugins
for name, hook in self._gcode_hooks[phase].items():
try:
hook_result = hook(self, phase, command, command_type, gcode)
except:
self._logger.exception("Error while processing hook {name} for phase {phase} and command {command}:".format(**locals()))
else:
command, command_type, gcode = self._handle_command_handler_result(command, command_type, gcode, hook_result)
if command is None:
# hook handler return None as command, so we'll stop here and return a full out None result
return None, None, None
# if it's a gcode command send it through the specific handler if it exists
if gcode is not None:
gcodeHandler = "_gcode_" + gcode + "_" + phase
if hasattr(self, gcodeHandler):
handler_result = getattr(self, gcodeHandler)(command, cmd_type=command_type)
command, command_type, gcode = self._handle_command_handler_result(command, command_type, gcode, handler_result)
# send it through the phase specific command handler if it exists
commandPhaseHandler = "_command_phase_" + phase
if hasattr(self, commandPhaseHandler):
handler_result = getattr(self, commandPhaseHandler)(command, cmd_type=command_type, gcode=gcode)
command, command_type, gcode = self._handle_command_handler_result(command, command_type, gcode, handler_result)
# finally return whatever we resulted on
return command, command_type, gcode
def _handle_command_handler_result(self, command, command_type, gcode, handler_result):
original_tuple = (command, command_type, gcode)
if handler_result is None:
# handler didn't return anything, we'll just continue
return original_tuple
if isinstance(handler_result, basestring):
# handler did return just a string, we'll turn that into a 1-tuple now
handler_result = (handler_result,)
elif not isinstance(handler_result, (tuple, list)):
# handler didn't return an expected result format, we'll just ignore it and continue
return original_tuple
hook_result_length = len(handler_result)
if hook_result_length == 1:
# handler returned just the command
command, = handler_result
elif hook_result_length == 2:
# handler returned command and command_type
command, command_type = handler_result
else:
# handler returned a tuple of an unexpected length
return original_tuple
gcode = gcode_command_for_cmd(command)
return command, command_type, gcode
##~~ actual sending via serial
def _doIncrementAndSendWithChecksum(self, cmd):
with self._line_mutex:
linenumber = self._currentLine
self._addToLastLines(cmd)
self._currentLine += 1
self._doSendWithChecksum(cmd, linenumber)
def _doSendWithChecksum(self, cmd, lineNumber):
commandToSend = "N%d %s" % (lineNumber, cmd)
checksum = reduce(lambda x,y:x^y, map(ord, commandToSend))
commandToSend = "%s*%d" % (commandToSend, checksum)
self._doSendWithoutChecksum(commandToSend)
def _doSendWithoutChecksum(self, cmd):
if self._serial is None:
return
self._log("Send: %s" % cmd)
try:
self._serial.write(cmd + '\n')
except serial.SerialTimeoutException:
self._log("Serial timeout while writing to serial port, trying again.")
try:
self._serial.write(cmd + '\n')
except:
if not self._connection_closing:
self._logger.exception("Unexpected error while writing to serial port")
self._log("Unexpected error while writing to serial port: %s" % (get_exception_string()))
self._errorValue = get_exception_string()
self.close(True)
except:
if not self._connection_closing:
self._logger.exception("Unexpected error while writing to serial port")
self._log("Unexpected error while writing to serial port: %s" % (get_exception_string()))
self._errorValue = get_exception_string()
self.close(True)
##~~ command handlers
def _gcode_T_sent(self, cmd, cmd_type=None):
toolMatch = regexes_parameters["intT"].search(cmd)
if toolMatch:
self._currentTool = int(toolMatch.group("value"))
def _gcode_G0_sent(self, cmd, cmd_type=None):
if 'Z' in cmd:
match = regexes_parameters["floatZ"].search(cmd)
if match:
try:
z = float(match.group("value"))
if self._currentZ != z:
self._currentZ = z
self._callback.on_comm_z_change(z)
except ValueError:
pass
_gcode_G1_sent = _gcode_G0_sent
def _gcode_M0_queuing(self, cmd, cmd_type=None):
self.setPause(True)
return None, # Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
_gcode_M1_queuing = _gcode_M0_queuing
def _gcode_M25_queuing(self, cmd, cmd_type=None):
# M25 while not printing from SD will be handled as pause. This way it can be used as another marker
# for GCODE induced pausing. Send it to the printer anyway though.
if self.isPrinting() and not self.isSdPrinting():
self.setPause(True)
def _gcode_M28_sent(self, cmd, cmd_type=None):
if not self.isStreaming():
self._log("Detected manual streaming. Disabling temperature polling. Finish writing with M29. Do NOT attempt to print while manually streaming!")
self._manualStreaming = True
def _gcode_M29_sent(self, cmd, cmd_type=None):
if self._manualStreaming:
self._log("Manual streaming done. Re-enabling temperature polling. All is well.")
self._manualStreaming = False
def _gcode_M140_queuing(self, cmd, cmd_type=None):
if not self._printerProfileManager.get_current_or_default()["heatedBed"]:
self._log("Warn: Not sending \"{}\", printer profile has no heated bed".format(cmd))
return None, # Don't send bed commands if we don't have a heated bed
_gcode_M190_queuing = _gcode_M140_queuing
def _gcode_M104_sent(self, cmd, cmd_type=None, wait=False):
toolNum = self._currentTool
toolMatch = regexes_parameters["intT"].search(cmd)
if toolMatch:
toolNum = int(toolMatch.group("value"))
if wait:
self._formerTool = self._currentTool
self._currentTool = toolNum
match = regexes_parameters["floatS"].search(cmd)
if match:
try:
target = float(match.group("value"))
if toolNum in self._temp.keys() and self._temp[toolNum] is not None and isinstance(self._temp[toolNum], tuple):
(actual, oldTarget) = self._temp[toolNum]
self._temp[toolNum] = (actual, target)
else:
self._temp[toolNum] = (None, target)
except ValueError:
pass
def _gcode_M140_sent(self, cmd, cmd_type=None, wait=False):
match = regexes_parameters["floatS"].search(cmd)
if match:
try:
target = float(match.group("value"))
if self._bedTemp is not None and isinstance(self._bedTemp, tuple):
(actual, oldTarget) = self._bedTemp
self._bedTemp = (actual, target)
else:
self._bedTemp = (None, target)
except ValueError:
pass
def _gcode_M109_sent(self, cmd, cmd_type=None):
self._heatupWaitStartTime = time.time()
self._long_running_command = True
self._heating = True
self._gcode_M104_sent(cmd, cmd_type, wait=True)
def _gcode_M190_sent(self, cmd, cmd_type=None):
self._heatupWaitStartTime = time.time()
self._long_running_command = True
self._heating = True
self._gcode_M140_sent(cmd, cmd_type, wait=True)
def _gcode_M110_sending(self, cmd, cmd_type=None):
newLineNumber = None
match = regexes_parameters["intN"].search(cmd)
if match:
try:
newLineNumber = int(match.group("value"))
except:
pass
else:
newLineNumber = 0
with self._line_mutex:
# send M110 command with new line number
self._currentLine = newLineNumber
# after a reset of the line number we have no way to determine what line exactly the printer now wants
self._lastLines.clear()
self._resendDelta = None
def _gcode_M112_queuing(self, cmd, cmd_type=None):
# emergency stop, jump the queue with the M112
self._doSendWithoutChecksum("M112")
self._doIncrementAndSendWithChecksum("M112")
# No idea if the printer is still listening or if M112 won. Just in case
# we'll now try to also manually make sure all heaters are shut off - better
# safe than sorry. We do this ignoring the queue since at this point it
# is irrelevant whether the printer has sent enough ack's or not, we
# are going to shutdown the connection in a second anyhow.
for tool in range(self._printerProfileManager.get_current_or_default()["extruder"]["count"]):
self._doIncrementAndSendWithChecksum("M104 T{tool} S0".format(tool=tool))
if self._printerProfileManager.get_current_or_default()["heatedBed"]:
self._doIncrementAndSendWithChecksum("M140 S0")
# close to reset host state
self._errorValue = "Closing serial port due to emergency stop M112."
self._log(self._errorValue)
self.close(isError=True)
# fire the M112 event since we sent it and we're going to prevent the caller from seeing it
gcode = "M112"
if gcode in gcodeToEvent:
eventManager().fire(gcodeToEvent[gcode])
# return None 1-tuple to eat the one that is queuing because we don't want to send it twice
# I hope it got it the first time because as far as I can tell, there is no way to know
return None,
def _gcode_G4_sent(self, cmd, cmd_type=None):
# we are intending to dwell for a period of time, increase the timeout to match
p_match = regexes_parameters["floatP"].search(cmd)
s_match = regexes_parameters["floatS"].search(cmd)
_timeout = 0
if p_match:
_timeout = float(p_match.group("value")) / 1000.0
elif s_match:
_timeout = float(s_match.group("value"))
self._timeout = get_new_timeout("communication") + _timeout
##~~ command phase handlers
def _command_phase_sending(self, cmd, cmd_type=None, gcode=None):
if gcode is not None and gcode in self._long_running_commands:
self._long_running_command = True
### MachineCom callback ################################################################################################
class MachineComPrintCallback(object):
def on_comm_log(self, message):
pass
def on_comm_temperature_update(self, temp, bedTemp):
pass
def on_comm_state_change(self, state):
pass
def on_comm_message(self, message):
pass
def on_comm_progress(self):
pass
def on_comm_print_job_done(self):
pass
def on_comm_z_change(self, newZ):
pass
def on_comm_file_selected(self, filename, filesize, sd):
pass
def on_comm_sd_state_change(self, sdReady):
pass
def on_comm_sd_files(self, files):
pass
def on_comm_file_transfer_started(self, filename, filesize):
pass
def on_comm_file_transfer_done(self, filename):
pass
def on_comm_force_disconnect(self):
pass
def on_comm_record_fileposition(self, origin, name, pos):
pass
### Printing file information classes ##################################################################################
class PrintingFileInformation(object):
"""
Encapsulates information regarding the current file being printed: file name, current position, total size and
time the print started.
Allows to reset the current file position to 0 and to calculate the current progress as a floating point
value between 0 and 1.
"""
def __init__(self, filename):
self._logger = logging.getLogger(__name__)
self._filename = filename
self._pos = 0
self._size = None
self._start_time = None
def getStartTime(self):
return self._start_time
def getFilename(self):
return self._filename
def getFilesize(self):
return self._size
def getFilepos(self):
return self._pos
def getFileLocation(self):
return FileDestinations.LOCAL
def getProgress(self):
"""
The current progress of the file, calculated as relation between file position and absolute size. Returns -1
if file size is None or < 1.
"""
if self._size is None or not self._size > 0:
return -1
return float(self._pos) / float(self._size)
def reset(self):
"""
Resets the current file position to 0.
"""
self._pos = 0
def start(self):
"""
Marks the print job as started and remembers the start time.
"""
self._start_time = time.time()
def close(self):
"""
Closes the print job.
"""
pass
class PrintingSdFileInformation(PrintingFileInformation):
"""
Encapsulates information regarding an ongoing print from SD.
"""
def __init__(self, filename, size):
PrintingFileInformation.__init__(self, filename)
self._size = size
def setFilepos(self, pos):
"""
Sets the current file position.
"""
self._pos = pos
def getFileLocation(self):
return FileDestinations.SDCARD
class PrintingGcodeFileInformation(PrintingFileInformation):
"""
Encapsulates information regarding an ongoing direct print. Takes care of the needed file handle and ensures
that the file is closed in case of an error.
"""
def __init__(self, filename, offsets_callback=None, current_tool_callback=None):
PrintingFileInformation.__init__(self, filename)
self._handle = None
self._first_line = None
self._offsets_callback = offsets_callback
self._current_tool_callback = current_tool_callback
if not os.path.exists(self._filename) or not os.path.isfile(self._filename):
raise IOError("File %s does not exist" % self._filename)
self._size = os.stat(self._filename).st_size
self._pos = 0
def seek(self, offset):
if self._handle is None:
return
self._handle.seek(offset)
self._pos = self._handle.tell()
def start(self):
"""
Opens the file for reading and determines the file size.
"""
PrintingFileInformation.start(self)
self._handle = bom_aware_open(self._filename, encoding="utf-8", errors="replace")
def close(self):
"""
Closes the file if it's still open.
"""
PrintingFileInformation.close(self)
if self._handle is not None:
try:
self._handle.close()
except:
pass
self._handle = None
def getNext(self):
"""
Retrieves the next line for printing.
"""
if self._handle is None:
raise ValueError("File %s is not open for reading" % self._filename)
try:
offsets = self._offsets_callback() if self._offsets_callback is not None else None
current_tool = self._current_tool_callback() if self._current_tool_callback is not None else None
processed = None
while processed is None:
if self._handle is None:
# file got closed just now
return None
line = to_unicode(self._handle.readline())
if not line:
self.close()
processed = process_gcode_line(line, offsets=offsets, current_tool=current_tool)
self._pos = self._handle.tell()
return processed
except Exception as e:
self.close()
self._logger.exception("Exception while processing line")
raise e
class StreamingGcodeFileInformation(PrintingGcodeFileInformation):
def __init__(self, path, localFilename, remoteFilename):
PrintingGcodeFileInformation.__init__(self, path)
self._localFilename = localFilename
self._remoteFilename = remoteFilename
def start(self):
PrintingGcodeFileInformation.start(self)
self._start_time = time.time()
def getLocalFilename(self):
return self._localFilename
def getRemoteFilename(self):
return self._remoteFilename
class TypedQueue(queue.Queue):
def __init__(self, maxsize=0):
queue.Queue.__init__(self, maxsize=maxsize)
self._lookup = []
def _put(self, item):
if isinstance(item, tuple) and len(item) == 3:
cmd, line, cmd_type = item
if cmd_type is not None:
if cmd_type in self._lookup:
raise TypeAlreadyInQueue(cmd_type, "Type {cmd_type} is already in queue".format(**locals()))
else:
self._lookup.append(cmd_type)
queue.Queue._put(self, item)
def _get(self):
item = queue.Queue._get(self)
if isinstance(item, tuple) and len(item) == 3:
cmd, line, cmd_type = item
if cmd_type is not None and cmd_type in self._lookup:
self._lookup.remove(cmd_type)
return item
class TypeAlreadyInQueue(Exception):
def __init__(self, t, *args, **kwargs):
Exception.__init__(self, *args, **kwargs)
self.type = t
def get_new_timeout(type):
now = time.time()
return now + get_interval(type)
def get_interval(type, default_value=0.0):
if type not in default_settings["serial"]["timeout"]:
return default_value
else:
value = settings().getFloat(["serial", "timeout", type])
if not value:
return default_value
else:
return value
_temp_command_regex = re.compile("^M(?P<command>104|109|140|190)(\s+T(?P<tool>\d+)|\s+S(?P<temperature>[-+]?\d*\.?\d*))+")
def apply_temperature_offsets(line, offsets, current_tool=None):
if offsets is None:
return line
match = _temp_command_regex.match(line)
if match is None:
return line
groups = match.groupdict()
if not "temperature" in groups or groups["temperature"] is None:
return line
offset = 0
if current_tool is not None and (groups["command"] == "104" or groups["command"] == "109"):
# extruder temperature, determine which one and retrieve corresponding offset
tool_num = current_tool
if "tool" in groups and groups["tool"] is not None:
tool_num = int(groups["tool"])
tool_key = "tool%d" % tool_num
offset = offsets[tool_key] if tool_key in offsets and offsets[tool_key] else 0
elif groups["command"] == "140" or groups["command"] == "190":
# bed temperature
offset = offsets["bed"] if "bed" in offsets else 0
if offset == 0:
return line
temperature = float(groups["temperature"])
if temperature == 0:
return line
return line[:match.start("temperature")] + "%f" % (temperature + offset) + line[match.end("temperature"):]
def strip_comment(line):
if not ";" in line:
# shortcut
return line
escaped = False
result = []
for c in line:
if c == ";" and not escaped:
break
result += c
escaped = (c == "\\") and not escaped
return "".join(result)
def process_gcode_line(line, offsets=None, current_tool=None):
line = strip_comment(line).strip()
if not len(line):
return None
if offsets is not None:
line = apply_temperature_offsets(line, offsets, current_tool=current_tool)
return line
def convert_pause_triggers(configured_triggers):
triggers = {
"enable": [],
"disable": [],
"toggle": []
}
for trigger in configured_triggers:
if not "regex" in trigger or not "type" in trigger:
continue
try:
regex = trigger["regex"]
t = trigger["type"]
if t in triggers:
# make sure regex is valid
re.compile(regex)
# add to type list
triggers[t].append(regex)
except:
# invalid regex or something like this, we'll just skip this entry
pass
result = dict()
for t in triggers.keys():
if len(triggers[t]) > 0:
result[t] = re.compile("|".join(map(lambda pattern: "({pattern})".format(pattern=pattern), triggers[t])))
return result
def convert_feedback_controls(configured_controls):
def preprocess_feedback_control(control, result):
if "key" in control and "regex" in control and "template" in control:
# key is always the md5sum of the regex
key = control["key"]
if result[key]["pattern"] is None or result[key]["matcher"] is None:
# regex has not been registered
try:
result[key]["matcher"] = re.compile(control["regex"])
result[key]["pattern"] = control["regex"]
except Exception as exc:
logging.getLogger(__name__).warn("Invalid regex {regex} for custom control: {exc}".format(regex=control["regex"], exc=str(exc)))
result[key]["templates"][control["template_key"]] = control["template"]
elif "children" in control:
for c in control["children"]:
preprocess_feedback_control(c, result)
def prepare_result_entry():
return dict(pattern=None, matcher=None, templates=dict())
from collections import defaultdict
feedback_controls = defaultdict(prepare_result_entry)
for control in configured_controls:
preprocess_feedback_control(control, feedback_controls)
feedback_pattern = []
for match_key, entry in feedback_controls.items():
if entry["matcher"] is None or entry["pattern"] is None:
continue
feedback_pattern.append("(?P<group{key}>{pattern})".format(key=match_key, pattern=entry["pattern"]))
feedback_matcher = re.compile("|".join(feedback_pattern))
return feedback_controls, feedback_matcher
def canonicalize_temperatures(parsed, current):
"""
Canonicalizes the temperatures provided in parsed.
Will make sure that returned result only contains extruder keys
like Tn, so always qualified with a tool number.
The algorithm for cleaning up the parsed keys is the following:
* If ``T`` is not included with the reported extruders, return
* If more than just ``T`` is reported:
* If both ``T`` and ``T0`` are reported set ``Tc`` to ``T``, remove
``T`` from the result.
* Else set ``T0`` to ``T`` and delete ``T`` (Smoothie extra).
* If only ``T`` is reported, set ``Tc`` to ``T`` and delete ``T``
* return
Arguments:
parsed (dict): the parsed temperatures (mapping tool => (actual, target))
to canonicalize
current (int): the current active extruder
Returns:
dict: the canonicalized version of ``parsed``
"""
reported_extruders = filter(lambda x: x.startswith("T"), parsed.keys())
if not "T" in reported_extruders:
# Our reported_extruders are either empty or consist purely
# of Tn keys, no need for any action
return parsed
current_tool_key = "T%d" % current
result = dict(parsed)
if len(reported_extruders) > 1:
if "T0" in reported_extruders:
# Both T and T0 are present, so T contains the current
# extruder's temperature, e.g. for current_tool == 1:
#
# T:<T1> T0:<T0> T2:<T2> ... B:<B>
#
# becomes
#
# T0:<T1> T1:<T1> T2:<T2> ... B:<B>
#
# Same goes if Tc is already present, it will be overwritten:
#
# T:<T1> T0:<T0> T1:<T1> T2:<T2> ... B:<B>
#
# becomes
#
# T0:<T0> T1:<T1> T2:<T2> ... B:<B>
result[current_tool_key] = result["T"]
del result["T"]
else:
# So T is there, but T0 isn't. That looks like Smoothieware which
# always reports the first extruder T0 as T:
#
# T:<T0> T1:<T1> T2:<T2> ... B:<B>
#
# becomes
#
# T0:<T0> T1:<T1> T2:<T2> ... B:<B>
result["T0"] = result["T"]
del result["T"]
else:
# We only have T. That can mean two things:
#
# * we only have one extruder at all, or
# * we are currently parsing a response to M109/M190, which on
# some firmwares doesn't report the full M105 output while
# waiting for the target temperature to be reached but only
# reports the current tool and bed
#
# In both cases it is however safe to just move our T over
# to T<current> in the parsed data, current should always stay
# 0 for single extruder printers. E.g. for current_tool == 1:
#
# T:<T1>
#
# becomes
#
# T1:<T1>
result[current_tool_key] = result["T"]
del result["T"]
return result
def parse_temperature_line(line, current):
"""
Parses the provided temperature line.
The result will be a dictionary mapping from the extruder or bed key to
a tuple with current and target temperature. The result will be canonicalized
with :func:`canonicalize_temperatures` before returning.
Arguments:
line (str): the temperature line to parse
current (int): the current active extruder
Returns:
tuple: a 2-tuple with the maximum tool number and a dict mapping from
key to (actual, target) tuples, with key either matching ``Tn`` for ``n >= 0`` or ``B``
"""
result = {}
maxToolNum = 0
for match in re.finditer(regex_temp, line):
values = match.groupdict()
tool = values["tool"]
toolnum = values.get("toolnum", None)
toolNumber = int(toolnum) if toolnum is not None and len(toolnum) else None
if toolNumber > maxToolNum:
maxToolNum = toolNumber
try:
actual = float(match.group(3))
target = None
if match.group(4) and match.group(5):
target = float(match.group(5))
result[tool] = (actual, target)
except ValueError:
# catch conversion issues, we'll rather just not get the temperature update instead of killing the connection
pass
return max(maxToolNum, current), canonicalize_temperatures(result, current)
def gcode_command_for_cmd(cmd):
"""
Tries to parse the provided ``cmd`` and extract the GCODE command identifier from it (e.g. "G0" for "G0 X10.0").
Arguments:
cmd (str): The command to try to parse.
Returns:
str or None: The GCODE command identifier if it could be parsed, or None if not.
"""
if not cmd:
return None
gcode = regex_command.search(cmd)
if not gcode:
return None
values = gcode.groupdict()
if "commandGM" in values and values["commandGM"]:
return values["commandGM"]
elif "commandT" in values and values["commandT"]:
return values["commandT"]
else:
# this should never happen
return None