Fixed & refactored temperature parsing
T-only lines were not properly parsed. Should now be fixed to be associated to currently selected extruder. Also added unit tests for the temperature parsing. While at it also extracted regexes and added unit tests for command parsing (gcode & parameters)
This commit is contained in:
parent
84968272aa
commit
f6a259fde3
2 changed files with 334 additions and 119 deletions
|
|
@ -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,7 +1379,7 @@ 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():
|
||||
|
|
@ -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