Merge branch 'devel' into dev/firstRunWizard
This commit is contained in:
commit
f1b2c43286
5 changed files with 350 additions and 124 deletions
|
|
@ -631,15 +631,23 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
result = dict(check)
|
||||
|
||||
if target == "octoprint":
|
||||
from octoprint._version import get_versions
|
||||
from flask.ext.babel import gettext
|
||||
result["displayName"] = check.get("displayName", gettext("OctoPrint"))
|
||||
result["displayVersion"] = check.get("displayVersion", "{octoprint_version}")
|
||||
result["current"] = get_versions()["version"]
|
||||
|
||||
from octoprint._version import get_versions
|
||||
versions = get_versions()
|
||||
if check["type"] == "github_commit":
|
||||
result["current"] = versions.get("full-revisionid", versions.get("full", "unknown"))
|
||||
else:
|
||||
result["current"] = versions["version"]
|
||||
else:
|
||||
result["displayName"] = check.get("displayName", target)
|
||||
result["displayVersion"] = check.get("displayVersion", check.get("current", "unknown"))
|
||||
result["current"] = check.get("current", check.get("displayVersion", None))
|
||||
if check["type"] in ("github_commit"):
|
||||
result["current"] = check.get("current", None)
|
||||
else:
|
||||
result["current"] = check.get("current", check.get("displayVersion", None))
|
||||
|
||||
return result
|
||||
|
||||
|
|
|
|||
|
|
@ -321,7 +321,7 @@ $(function() {
|
|||
.data("contextParent", $(this))
|
||||
.show()
|
||||
.css({
|
||||
position: "absolute",
|
||||
position: "fixed",
|
||||
left: getMenuPosition(e.clientX, 'width', 'scrollLeft'),
|
||||
top: getMenuPosition(e.clientY, 'height', 'scrollTop'),
|
||||
"z-index": 9999
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
<div style="display: none;" data-bind="visible: loginState.isUser">
|
||||
<div class="alert" data-bind="visible: isPrinting()">
|
||||
{{ _('Take note that timelapse configuration is disabled while your printer is printing.') }}
|
||||
</div>
|
||||
<h1>{{ _('Timelapse Configuration') }}</h1>
|
||||
|
||||
<label for="webcam_timelapse_mode">{{ _('Timelapse Mode') }}</label>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,77 @@ except:
|
|||
|
||||
_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":
|
||||
|
|
@ -210,28 +281,6 @@ class MachineCom(object):
|
|||
self._currentFile = None
|
||||
|
||||
# regexes
|
||||
floatPattern = "[-+]?[0-9]*\.?[0-9]+"
|
||||
positiveFloatPattern = "[+]?[0-9]*\.?[0-9]+"
|
||||
intPattern = "\d+"
|
||||
self._regex_command = re.compile("^\s*([GM]\d+|T)")
|
||||
self._regex_float = re.compile(floatPattern)
|
||||
self._regex_paramZFloat = re.compile("Z(%s)" % floatPattern)
|
||||
self._regex_paramSInt = re.compile("S(%s)" % intPattern)
|
||||
self._regex_paramNInt = re.compile("N(%s)" % intPattern)
|
||||
self._regex_paramTInt = re.compile("T(%s)" % intPattern)
|
||||
self._regex_minMaxError = re.compile("Error:[0-9]\n")
|
||||
self._regex_sdPrintingByte = re.compile("([0-9]*)/([0-9]*)")
|
||||
self._regex_sdFileOpened = re.compile("File opened:\s*(.*?)\s+Size:\s*(%s)" % intPattern)
|
||||
|
||||
# Regex matching temperature entries in line. Groups will be as follows:
|
||||
# - 1: whole tool designator incl. optional toolNumber ("T", "Tn", "B")
|
||||
# - 2: toolNumber, if given ("", "n", "")
|
||||
# - 3: actual temperature
|
||||
# - 4: whole target substring, if given (e.g. " / 22.0")
|
||||
# - 5: target temperature
|
||||
self._regex_temp = re.compile("(B|T(\d*)):\s*(%s)(\s*\/?\s*(%s))?" % (positiveFloatPattern, positiveFloatPattern))
|
||||
self._regex_repetierTempExtr = re.compile("TargetExtr([0-9]+):(%s)" % positiveFloatPattern)
|
||||
self._regex_repetierTempBed = re.compile("TargetBed:(%s)" % positiveFloatPattern)
|
||||
|
||||
self._long_running_commands = settings().get(["serial", "longRunningCommands"])
|
||||
|
||||
|
|
@ -783,51 +832,9 @@ class MachineCom(object):
|
|||
|
||||
##~~ communication monitoring and handling
|
||||
|
||||
def _parseTemperatures(self, line):
|
||||
result = {}
|
||||
maxToolNum = 0
|
||||
for match in re.finditer(self._regex_temp, line):
|
||||
tool = match.group(1)
|
||||
toolNumber = int(match.group(2)) if match.group(2) and len(match.group(2)) > 0 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] = (toolNumber, actual, target)
|
||||
except ValueError:
|
||||
# catch conversion issues, we'll rather just not get the temperature update instead of killing the connection
|
||||
pass
|
||||
|
||||
if "T0" in result.keys() and "T" in result.keys():
|
||||
del result["T"]
|
||||
|
||||
return maxToolNum, result
|
||||
|
||||
def _processTemperatures(self, line):
|
||||
maxToolNum, parsedTemps = self._parseTemperatures(line)
|
||||
|
||||
# extruder temperatures
|
||||
if not "T0" in parsedTemps.keys() and not "T1" in parsedTemps.keys() and "T" in parsedTemps.keys():
|
||||
# no T1 so only single reporting, "T" is our one and only extruder temperature
|
||||
toolNum, actual, target = parsedTemps["T"]
|
||||
|
||||
if target is not None:
|
||||
self._temp[0] = (actual, target)
|
||||
elif 0 in self._temp.keys() and self._temp[0] is not None and isinstance(self._temp[0], tuple):
|
||||
(oldActual, oldTarget) = self._temp[0]
|
||||
self._temp[0] = (actual, oldTarget)
|
||||
else:
|
||||
self._temp[0] = (actual, None)
|
||||
elif not "T0" in parsedTemps.keys() and "T" in parsedTemps.keys():
|
||||
# Smoothieware sends multi extruder temperature data this way: "T:<first extruder> T1:<second extruder> ..." and therefore needs some special treatment...
|
||||
_, actual, target = parsedTemps["T"]
|
||||
del parsedTemps["T"]
|
||||
parsedTemps["T0"] = (0, actual, target)
|
||||
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):
|
||||
|
|
@ -835,18 +842,18 @@ class MachineCom(object):
|
|||
if not tool in parsedTemps.keys():
|
||||
continue
|
||||
|
||||
toolNum, actual, target = parsedTemps[tool]
|
||||
actual, target = parsedTemps[tool]
|
||||
if target is not None:
|
||||
self._temp[toolNum] = (actual, target)
|
||||
elif toolNum in self._temp.keys() and self._temp[toolNum] is not None and isinstance(self._temp[toolNum], tuple):
|
||||
(oldActual, oldTarget) = self._temp[toolNum]
|
||||
self._temp[toolNum] = (actual, oldTarget)
|
||||
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[toolNum] = (actual, None)
|
||||
self._temp[n] = (actual, None)
|
||||
|
||||
# bed temperature
|
||||
if "B" in parsedTemps.keys():
|
||||
toolNum, actual, target = parsedTemps["B"]
|
||||
actual, target = parsedTemps["B"]
|
||||
if target is not None:
|
||||
self._bedTemp = (actual, target)
|
||||
elif self._bedTemp is not None and isinstance(self._bedTemp, tuple):
|
||||
|
|
@ -973,8 +980,8 @@ class MachineCom(object):
|
|||
self._callback.on_comm_temperature_update(self._temp, self._bedTemp)
|
||||
|
||||
elif supportRepetierTargetTemp and ('TargetExtr' in line or 'TargetBed' in line):
|
||||
matchExtr = self._regex_repetierTempExtr.match(line)
|
||||
matchBed = self._regex_repetierTempBed.match(line)
|
||||
matchExtr = regex_repetierTempExtr.match(line)
|
||||
matchBed = regex_repetierTempBed.match(line)
|
||||
|
||||
if matchExtr is not None:
|
||||
toolNum = int(matchExtr.group(1))
|
||||
|
|
@ -1028,18 +1035,18 @@ class MachineCom(object):
|
|||
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 = self._regex_sdPrintingByte.search(line)
|
||||
self._currentFile.setFilepos(int(match.group(1)))
|
||||
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 = self._regex_sdFileOpened.search(line)
|
||||
match = regex_sdFileOpened.search(line)
|
||||
if self._sdFileToSelect:
|
||||
name = self._sdFileToSelect
|
||||
self._sdFileToSelect = None
|
||||
else:
|
||||
name = match.group(1)
|
||||
self._currentFile = PrintingSdFileInformation(name, int(match.group(2)))
|
||||
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
|
||||
|
|
@ -1372,10 +1379,10 @@ class MachineCom(object):
|
|||
# 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 self._regex_minMaxError.match(line):
|
||||
if regex_minMaxError.match(line):
|
||||
line = line.rstrip() + self._readline()
|
||||
|
||||
if 'line number' in line.lower() or 'checksum' in line.lower() or 'expected line' in line.lower():
|
||||
if 'line number' in line.lower() or 'checksum' in line.lower() or 'format error' in line.lower() or 'expected line' in line.lower():
|
||||
#Skip the communication errors, as those get corrected.
|
||||
self._lastCommError = line[6:] if line.startswith("Error:") else line[2:]
|
||||
pass
|
||||
|
|
@ -1536,25 +1543,6 @@ class MachineCom(object):
|
|||
# trigger the "queued" phase only if we are not streaming to sd right now
|
||||
self._process_command_phase("queued", cmd, cmd_type, gcode=gcode)
|
||||
|
||||
def gcode_command_for_cmd(self, 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 = self._regex_command.search(cmd)
|
||||
if not gcode:
|
||||
return None
|
||||
|
||||
return gcode.group(1)
|
||||
|
||||
##~~ send loop handling
|
||||
|
||||
def _enqueue_for_sending(self, command, linenumber=None, command_type=None):
|
||||
|
|
@ -1596,7 +1584,7 @@ class MachineCom(object):
|
|||
# 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 = self.gcode_command_for_cmd(command)
|
||||
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
|
||||
|
|
@ -1657,7 +1645,7 @@ class MachineCom(object):
|
|||
return command, command_type, gcode
|
||||
|
||||
if gcode is None:
|
||||
gcode = self.gcode_command_for_cmd(command)
|
||||
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():
|
||||
|
|
@ -1712,7 +1700,7 @@ class MachineCom(object):
|
|||
# handler returned a tuple of an unexpected length
|
||||
return original_tuple
|
||||
|
||||
gcode = self.gcode_command_for_cmd(command)
|
||||
gcode = gcode_command_for_cmd(command)
|
||||
return command, command_type, gcode
|
||||
|
||||
##~~ actual sending via serial
|
||||
|
|
@ -1750,13 +1738,13 @@ class MachineCom(object):
|
|||
##~~ command handlers
|
||||
|
||||
def _gcode_T_sent(self, cmd, cmd_type=None):
|
||||
toolMatch = self._regex_paramTInt.search(cmd)
|
||||
toolMatch = regexes_parameters["intT"].search(cmd)
|
||||
if toolMatch:
|
||||
self._currentTool = int(toolMatch.group(1))
|
||||
|
||||
def _gcode_G0_sent(self, cmd, cmd_type=None):
|
||||
if 'Z' in cmd:
|
||||
match = self._regex_paramZFloat.search(cmd)
|
||||
match = regexes_parameters["floatZ"].search(cmd)
|
||||
if match:
|
||||
try:
|
||||
z = float(match.group(1))
|
||||
|
|
@ -1774,10 +1762,10 @@ class MachineCom(object):
|
|||
|
||||
def _gcode_M104_sent(self, cmd, cmd_type=None):
|
||||
toolNum = self._currentTool
|
||||
toolMatch = self._regex_paramTInt.search(cmd)
|
||||
toolMatch = regexes_parameters["intT"].search(cmd)
|
||||
if toolMatch:
|
||||
toolNum = int(toolMatch.group(1))
|
||||
match = self._regex_paramSInt.search(cmd)
|
||||
match = regexes_parameters["floatS"].search(cmd)
|
||||
if match:
|
||||
try:
|
||||
target = float(match.group(1))
|
||||
|
|
@ -1790,7 +1778,7 @@ class MachineCom(object):
|
|||
pass
|
||||
|
||||
def _gcode_M140_sent(self, cmd, cmd_type=None):
|
||||
match = self._regex_paramSInt.search(cmd)
|
||||
match = regexes_parameters["floatS"].search(cmd)
|
||||
if match:
|
||||
try:
|
||||
target = float(match.group(1))
|
||||
|
|
@ -1816,10 +1804,10 @@ class MachineCom(object):
|
|||
|
||||
def _gcode_M110_sending(self, cmd, cmd_type=None):
|
||||
newLineNumber = None
|
||||
match = self._regex_paramNInt.search(cmd)
|
||||
match = regexes_parameters["intN"].search(cmd)
|
||||
if match:
|
||||
try:
|
||||
newLineNumber = int(match.group(1))
|
||||
newLineNumber = int(match.group("value"))
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
|
|
@ -1837,16 +1825,14 @@ class MachineCom(object):
|
|||
|
||||
def _gcode_G4_sent(self, cmd, cmd_type=None):
|
||||
# we are intending to dwell for a period of time, increase the timeout to match
|
||||
cmd = cmd.upper()
|
||||
p_idx = cmd.find('P')
|
||||
s_idx = cmd.find('S')
|
||||
p_match = regexes_parameters["floatP"].search(cmd)
|
||||
s_match = regexes_parameters["floatS"].search(cmd)
|
||||
|
||||
_timeout = 0
|
||||
if p_idx != -1:
|
||||
# dwell time is specified in milliseconds
|
||||
_timeout = float(cmd[p_idx+1:]) / 1000.0
|
||||
elif s_idx != -1:
|
||||
# dwell time is specified in seconds
|
||||
_timeout = float(cmd[s_idx+1:])
|
||||
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
|
||||
|
|
@ -2236,3 +2222,159 @@ def convert_feedback_controls(configured_controls):
|
|||
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -156,3 +156,76 @@ class TestCommHelpers(unittest.TestCase):
|
|||
self.assertEquals(x_template, x["templates"][x_template_key])
|
||||
|
||||
self.assertEquals("(?P<group{temp_key}>{temp_regex})|(?P<group{x_key}>{x_regex})".format(**locals()), matcher.pattern)
|
||||
|
||||
@data(
|
||||
("G4 P2.0", "floatP", True, "2.0"),
|
||||
("M109 S220.0", "floatS", True, "220.0"),
|
||||
("G1 X10.0 Y10.0 Z0.2", "floatZ", True, "0.2"),
|
||||
("G1X10.0Y10.0Z0.2", "floatZ", True, "0.2"),
|
||||
("g1x10.0y10.0z0.2", "floatZ", True, "0.2"),
|
||||
("M110 N0", "intN", True, "0"),
|
||||
("M104 S220.0 T1", "intT", True, "1"),
|
||||
("M104 T1 S220.0", "intT", True, "1"),
|
||||
("N100 M110", "intN", True, "100"),
|
||||
("NP100", "floatP", False, None),
|
||||
)
|
||||
@unpack
|
||||
def test_parameter_regexes(self, line, parameter, should_match, expected_value):
|
||||
from octoprint.util.comm import regexes_parameters
|
||||
|
||||
regex = regexes_parameters[parameter]
|
||||
match = regex.search(line)
|
||||
|
||||
if should_match:
|
||||
self.assertIsNotNone(match)
|
||||
self.assertEquals(expected_value, match.group("value"))
|
||||
else:
|
||||
self.assertIsNone(match)
|
||||
|
||||
@data(
|
||||
("G0 X0", "G0"),
|
||||
("G28 X0 Y0", "G28"),
|
||||
("M109 S220.0 T1", "M109"),
|
||||
("M117 Hello World", "M117"),
|
||||
("T0", "T"),
|
||||
("T3", "T"),
|
||||
(None, None),
|
||||
("No match", None)
|
||||
)
|
||||
@unpack
|
||||
def test_gcode_command_for_cmd(self, cmd, expected):
|
||||
from octoprint.util.comm import gcode_command_for_cmd
|
||||
result = gcode_command_for_cmd(cmd)
|
||||
self.assertEquals(expected, result)
|
||||
|
||||
@data(
|
||||
("T:23.0 B:60.0", 0, dict(T0=(23.0, None), B=(60.0, None)), 0),
|
||||
("T:23.0 B:60.0", 1, dict(T1=(23.0, None), B=(60.0, None)), 1),
|
||||
("T:23.0/220.0 B:60.0/70.0", 0, dict(T0=(23.0, 220.0), B=(60.0, 70.0)), 0),
|
||||
("ok T:23.0/220.0 T0:23.0/220.0 T1:50.2/210.0 T2:39.4/220.0 B:60.0", 0, dict(T0=(23.0, 220.0), T1=(50.2, 210.0), T2=(39.4, 220.0), B=(60.0, None)), 2),
|
||||
("ok T:50.2/210.0 T0:23.0/220.0 T1:50.2/210.0 T2:39.4/220.0 B:60.0", 1, dict(T0=(23.0, 220.0), T1=(50.2, 210.0), T2=(39.4, 220.0), B=(60.0, None)), 2)
|
||||
)
|
||||
@unpack
|
||||
def test_process_temperature_line(self, line, current, expected_result, expected_max):
|
||||
from octoprint.util.comm import parse_temperature_line
|
||||
maxtool, result = parse_temperature_line(line, current)
|
||||
self.assertDictEqual(expected_result, result)
|
||||
self.assertEquals(expected_max, maxtool)
|
||||
|
||||
@data(
|
||||
(dict(T=(23.0,None)), 0, dict(T0=(23.0, None))),
|
||||
(dict(T=(23.0,None)), 1, dict(T1=(23.0, None))),
|
||||
(dict(T=(23.0, None), T0=(23.0, None), T1=(42.0, None)), 0, dict(T0=(23.0, None), T1=(42.0, None))),
|
||||
(dict(T=(42.0, None), T0=(23.0, None), T1=(42.0, None)), 1, dict(T0=(23.0, None), T1=(42.0, None))),
|
||||
(dict(T=(21.0, None), T0=(23.0, None), T1=(42.0, None)), 0, dict(T0=(21.0, None), T1=(42.0, None))),
|
||||
(dict(T=(41.0, None), T0=(23.0, None), T1=(42.0, None)), 1, dict(T0=(23.0, None), T1=(41.0, None))),
|
||||
(dict(T=(23.0, None), T1=(42.0, None)), 1, dict(T0=(23.0, None), T1=(42.0, None))),
|
||||
(dict(T0=(23.0, None), T1=(42.0, None)), 1, dict(T0=(23.0, None), T1=(42.0, None)))
|
||||
|
||||
)
|
||||
@unpack
|
||||
def test_canonicalize_temperatures(self, parsed, current, expected):
|
||||
from octoprint.util.comm import canonicalize_temperatures
|
||||
result = canonicalize_temperatures(parsed, current)
|
||||
self.assertDictEqual(expected, result)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue