diff --git a/src/pygcode/__init__.py b/src/pygcode/__init__.py index 51c75a7..27611cb 100644 --- a/src/pygcode/__init__.py +++ b/src/pygcode/__init__.py @@ -34,6 +34,7 @@ __all__ = [ 'Block', # Comment 'Comment', 'split_line', + # Word 'Word', 'text2words', 'str2word', 'words2dict', diff --git a/src/pygcode/block.py b/src/pygcode/block.py index ab9e774..74ff168 100644 --- a/src/pygcode/block.py +++ b/src/pygcode/block.py @@ -1,11 +1,13 @@ import re -from .words import text2words, WORD_MAP +from .words import text2words from .gcodes import words2gcodes +from . import dialects + class Block(object): """GCode block (effectively any gcode file line that defines any )""" - def __init__(self, text=None, verify=True): + def __init__(self, text=None, dialect=None, verify=True): """ Block Constructor :param text: gcode line content (including comments) as string @@ -26,6 +28,12 @@ class Block(object): self.gcodes = [] self.modal_params = [] + if dialect is None: + dialect = dialects.get_default() + self.dialect = dialect + + self._word_map = getattr(getattr(dialects, dialect), 'WORD_MAP') + # clean up block string if text: self._raw_text = text # unaltered block content (before alteration) @@ -71,7 +79,7 @@ class Block(object): modal_groups.add(gc.modal_group) def __getattr__(self, k): - if k in WORD_MAP: + if k in self._word_map: for w in self.words: if w.letter == k: return w diff --git a/src/pygcode/dialect/__init__.py b/src/pygcode/dialect/__init__.py deleted file mode 100644 index 9b083bc..0000000 --- a/src/pygcode/dialect/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -__all__ = [ - - 'DEFAULT', - - # registration decorators - 'gcode_dialect', - 'word_dialect', -] - -DEFAULT = 'linuxcnc' - -# Registration decorators -from .mapping import gcode_dialect -from .mapping import word_dialect diff --git a/src/pygcode/dialect/linuxcnc.py b/src/pygcode/dialect/linuxcnc.py deleted file mode 100644 index 6c564ff..0000000 --- a/src/pygcode/dialect/linuxcnc.py +++ /dev/null @@ -1,18 +0,0 @@ -""" -LinuxCNC - -The linuxcnc gcode dialect is typically used for subtractive fabrication, such -as milling. - -This dialect is the basis for all other dialects; GCodes and Words in other -dialects either inherit, or directly reference these classes. - -**Specification:** http://www.linuxcnc.org - -TODO: verify above info before publishing -""" - -# ======================== WORDS ======================== - - -# ======================== G-CODES ======================== diff --git a/src/pygcode/dialects/__init__.py b/src/pygcode/dialects/__init__.py new file mode 100644 index 0000000..9a079a3 --- /dev/null +++ b/src/pygcode/dialects/__init__.py @@ -0,0 +1,55 @@ +__all__ = [ + 'DEFAULT', + + # registration decorators + 'gcode_dialect', + 'word_dialect', + + # dialects + 'linuxcnc', + 'reprap', +] + + +# Registration decorators +from .mapping import gcode_dialect +from .mapping import word_dialect + +# Dialects +from . import linuxcnc +from . import reprap + + +_DEFAULT = 'linuxcnc' + + +def get_default(): + """ + Get the default gcode interpreter dialect. + (see :meth:`set_default` for details) + """ + return _DEFAULT + + +def set_default(name): + """ + Set the default gcode interpreter dialect. + This dialect will be used if no other is specified for particular function + calls. + + :param name: name of dialect + :type name: :class:`str` + + .. doctest:: + + >>> from pygcode import dialect + >>> dialect.get_default() + 'linuxcnc' + >>> dialect.set_default('reprap') + >>> dialect.get_default() + 'reprap' + + """ + + # TODO: verify valid name + _DEFAULT = name diff --git a/src/pygcode/dialects/linuxcnc.py b/src/pygcode/dialects/linuxcnc.py new file mode 100644 index 0000000..7dbe096 --- /dev/null +++ b/src/pygcode/dialects/linuxcnc.py @@ -0,0 +1,216 @@ +""" +LinuxCNC + +The linuxcnc gcode dialect is typically used for subtractive fabrication, such +as milling. + +This dialect is the basis for all other dialects; GCodes and Words in other +dialects either inherit, or directly reference these classes. + +**Specification:** http://www.linuxcnc.org + +TODO: verify above info before publishing +""" + +import re + +from .utils import WordType + +# ======================== WORDS ======================== + +REGEX_FLOAT = re.compile(r'^\s*-?(\d+\.?\d*|\.\d+)') # testcase: ..tests.test_words.WordValueMatchTests.test_float +REGEX_INT = re.compile(r'^\s*-?\d+') +REGEX_POSITIVEINT = re.compile(r'^\s*\d+') +REGEX_CODE = re.compile(r'^\s*\d+(\.\d)?') # float, but can't be negative + +# Value cleaning functions +def _clean_codestr(value): + if value < 10: + return "0%g" % value + return "%g" % value + +CLEAN_NONE = lambda v: v +CLEAN_FLOAT = lambda v: "{0:g}".format(round(v, 3)) +CLEAN_CODE = _clean_codestr +CLEAN_INT = lambda v: "%g" % v + +WORD_MAP = { + # Descriptions copied from wikipedia: + # https://en.wikipedia.org/wiki/G-code#Letter_addresses + + # Rotational Axes + 'A': WordType( + cls=float, + value_regex=REGEX_FLOAT, + description="Absolute or incremental position of A axis (rotational axis around X axis)", + clean_value=CLEAN_FLOAT, + ), + 'B': WordType( + cls=float, + value_regex=REGEX_FLOAT, + description="Absolute or incremental position of B axis (rotational axis around Y axis)", + clean_value=CLEAN_FLOAT, + ), + 'C': WordType( + cls=float, + value_regex=REGEX_FLOAT, + description="Absolute or incremental position of C axis (rotational axis around Z axis)", + clean_value=CLEAN_FLOAT, + ), + 'D': WordType( + cls=float, + value_regex=REGEX_FLOAT, + description="Defines diameter or radial offset used for cutter compensation. D is used for depth of cut on lathes. It is used for aperture selection and commands on photoplotters.", + clean_value=CLEAN_FLOAT, + ), + # Feed Rates + 'E': WordType( + cls=float, + value_regex=REGEX_FLOAT, + description="Precision feedrate for threading on lathes", + clean_value=CLEAN_FLOAT, + ), + 'F': WordType( + cls=float, + value_regex=REGEX_FLOAT, + description="Feedrate", + clean_value=CLEAN_FLOAT, + ), + # G-Codes + 'G': WordType( + cls=float, + value_regex=REGEX_CODE, + description="Address for preparatory commands", + clean_value=CLEAN_CODE, + ), + # Tool Offsets + 'H': WordType( + cls=float, + value_regex=REGEX_FLOAT, + description="Defines tool length offset; Incremental axis corresponding to C axis (e.g., on a turn-mill)", + clean_value=CLEAN_FLOAT, + ), + # Arc radius center coords + 'I': WordType( + cls=float, + value_regex=REGEX_FLOAT, + description="Defines arc center in X axis for G02 or G03 arc commands. Also used as a parameter within some fixed cycles.", + clean_value=CLEAN_FLOAT, + ), + 'J': WordType( + cls=float, + value_regex=REGEX_FLOAT, + description="Defines arc center in Y axis for G02 or G03 arc commands. Also used as a parameter within some fixed cycles.", + clean_value=CLEAN_FLOAT, + ), + 'K': WordType( + cls=float, + value_regex=REGEX_FLOAT, + description="Defines arc center in Z axis for G02 or G03 arc commands. Also used as a parameter within some fixed cycles, equal to L address.", + clean_value=CLEAN_FLOAT, + ), + # Loop Count + 'L': WordType( + cls=int, + value_regex=REGEX_POSITIVEINT, + description="Fixed cycle loop count; Specification of what register to edit using G10", + clean_value=CLEAN_INT, + ), + # Miscellaneous Function + 'M': WordType( + cls=float, + value_regex=REGEX_CODE, + description="Miscellaneous function", + clean_value=CLEAN_CODE, + ), + # Line Number + 'N': WordType( + cls=int, + value_regex=REGEX_POSITIVEINT, + description="Line (block) number in program; System parameter number to change using G10", + clean_value=CLEAN_INT, + ), + # Program Name + 'O': WordType( + cls=str, + value_regex=re.compile(r'^.+$'), # all the way to the end + description="Program name", + clean_value=CLEAN_NONE, + ), + # Parameter (arbitrary parameter) + 'P': WordType( + cls=float, # parameter is often an integer, but can be a float + value_regex=REGEX_FLOAT, + description="Serves as parameter address for various G and M codes", + clean_value=CLEAN_FLOAT, + ), + # Peck increment + 'Q': WordType( + cls=float, + value_regex=REGEX_FLOAT, + description="Depth to increase on each peck; Peck increment in canned cycles", + clean_value=CLEAN_FLOAT, + ), + # Arc Radius + 'R': WordType( + cls=float, + value_regex=REGEX_FLOAT, + description="Defines size of arc radius, or defines retract height in milling canned cycles", + clean_value=CLEAN_FLOAT, + ), + # Spindle speed + 'S': WordType( + cls=float, + value_regex=REGEX_FLOAT, + description="Defines speed, either spindle speed or surface speed depending on mode", + clean_value=CLEAN_FLOAT, + ), + # Tool Selecton + 'T': WordType( + cls=str, + value_regex=REGEX_POSITIVEINT, # tool string may have leading '0's, but is effectively an index (integer) + description="Tool selection", + clean_value=CLEAN_NONE, + ), + # Incremental axes + 'U': WordType( + cls=float, + value_regex=REGEX_FLOAT, + description="Incremental axis corresponding to X axis (typically only lathe group A controls) Also defines dwell time on some machines (instead of 'P' or 'X').", + clean_value=CLEAN_FLOAT, + ), + 'V': WordType( + cls=float, + value_regex=REGEX_FLOAT, + description="Incremental axis corresponding to Y axis", + clean_value=CLEAN_FLOAT, + ), + 'W': WordType( + cls=float, + value_regex=REGEX_FLOAT, + description="Incremental axis corresponding to Z axis (typically only lathe group A controls)", + clean_value=CLEAN_FLOAT, + ), + # Linear Axes + 'X': WordType( + cls=float, + value_regex=REGEX_FLOAT, + description="Absolute or incremental position of X axis.", + clean_value=CLEAN_FLOAT, + ), + 'Y': WordType( + cls=float, + value_regex=REGEX_FLOAT, + description="Absolute or incremental position of Y axis.", + clean_value=CLEAN_FLOAT, + ), + 'Z': WordType( + cls=float, + value_regex=REGEX_FLOAT, + description="Absolute or incremental position of Z axis.", + clean_value=CLEAN_FLOAT, + ), +} + + +# ======================== G-CODES ======================== diff --git a/src/pygcode/dialect/mapping.py b/src/pygcode/dialects/mapping.py similarity index 100% rename from src/pygcode/dialect/mapping.py rename to src/pygcode/dialects/mapping.py diff --git a/src/pygcode/dialect/reprap.py b/src/pygcode/dialects/reprap.py similarity index 100% rename from src/pygcode/dialect/reprap.py rename to src/pygcode/dialects/reprap.py diff --git a/src/pygcode/dialects/utils.py b/src/pygcode/dialects/utils.py new file mode 100644 index 0000000..a1738af --- /dev/null +++ b/src/pygcode/dialects/utils.py @@ -0,0 +1,9 @@ + +# Data Classes + +class WordType(object): + def __init__(self, cls, value_regex, description, clean_value): + self.cls = cls + self.value_regex = value_regex + self.description = description + self.clean_value = clean_value diff --git a/src/pygcode/words.py b/src/pygcode/words.py index c3dc5ed..63cd312 100644 --- a/src/pygcode/words.py +++ b/src/pygcode/words.py @@ -2,218 +2,31 @@ import re import itertools import six +from . import dialects from .exceptions import GCodeBlockFormatError, GCodeWordStrError -REGEX_FLOAT = re.compile(r'^\s*-?(\d+\.?\d*|\.\d+)') # testcase: ..tests.test_words.WordValueMatchTests.test_float -REGEX_INT = re.compile(r'^\s*-?\d+') -REGEX_POSITIVEINT = re.compile(r'^\s*\d+') -REGEX_CODE = re.compile(r'^\s*\d+(\.\d)?') # float, but can't be negative - -# Value cleaning functions -def _clean_codestr(value): - if value < 10: - return "0%g" % value - return "%g" % value - -CLEAN_NONE = lambda v: v -CLEAN_FLOAT = lambda v: "{0:g}".format(round(v, 3)) -CLEAN_CODE = _clean_codestr -CLEAN_INT = lambda v: "%g" % v - -WORD_MAP = { - # Descriptions copied from wikipedia: - # https://en.wikipedia.org/wiki/G-code#Letter_addresses - - # Rotational Axes - 'A': { - 'class': float, - 'value_regex': REGEX_FLOAT, - 'description': "Absolute or incremental position of A axis (rotational axis around X axis)", - 'clean_value': CLEAN_FLOAT, - }, - 'B': { - 'class': float, - 'value_regex': REGEX_FLOAT, - 'description': "Absolute or incremental position of B axis (rotational axis around Y axis)", - 'clean_value': CLEAN_FLOAT, - }, - 'C': { - 'class': float, - 'value_regex': REGEX_FLOAT, - 'description': "Absolute or incremental position of C axis (rotational axis around Z axis)", - 'clean_value': CLEAN_FLOAT, - }, - 'D': { - 'class': float, - 'value_regex': REGEX_FLOAT, - 'description': "Defines diameter or radial offset used for cutter compensation. D is used for depth of cut on lathes. It is used for aperture selection and commands on photoplotters.", - 'clean_value': CLEAN_FLOAT, - }, - # Feed Rates - 'E': { - 'class': float, - 'value_regex': REGEX_FLOAT, - 'description': "Precision feedrate for threading on lathes", - 'clean_value': CLEAN_FLOAT, - }, - 'F': { - 'class': float, - 'value_regex': REGEX_FLOAT, - 'description': "Feedrate", - 'clean_value': CLEAN_FLOAT, - }, - # G-Codes - 'G': { - 'class': float, - 'value_regex': REGEX_CODE, - 'description': "Address for preparatory commands", - 'clean_value': CLEAN_CODE, - }, - # Tool Offsets - 'H': { - 'class': float, - 'value_regex': REGEX_FLOAT, - 'description': "Defines tool length offset; Incremental axis corresponding to C axis (e.g., on a turn-mill)", - 'clean_value': CLEAN_FLOAT, - }, - # Arc radius center coords - 'I': { - 'class': float, - 'value_regex': REGEX_FLOAT, - 'description': "Defines arc center in X axis for G02 or G03 arc commands. Also used as a parameter within some fixed cycles.", - 'clean_value': CLEAN_FLOAT, - }, - 'J': { - 'class': float, - 'value_regex': REGEX_FLOAT, - 'description': "Defines arc center in Y axis for G02 or G03 arc commands. Also used as a parameter within some fixed cycles.", - 'clean_value': CLEAN_FLOAT, - }, - 'K': { - 'class': float, - 'value_regex': REGEX_FLOAT, - 'description': "Defines arc center in Z axis for G02 or G03 arc commands. Also used as a parameter within some fixed cycles, equal to L address.", - 'clean_value': CLEAN_FLOAT, - }, - # Loop Count - 'L': { - 'class': int, - 'value_regex': REGEX_POSITIVEINT, - 'description': "Fixed cycle loop count; Specification of what register to edit using G10", - 'clean_value': CLEAN_INT, - }, - # Miscellaneous Function - 'M': { - 'class': float, - 'value_regex': REGEX_CODE, - 'description': "Miscellaneous function", - 'clean_value': CLEAN_CODE, - }, - # Line Number - 'N': { - 'class': int, - 'value_regex': REGEX_POSITIVEINT, - 'description': "Line (block) number in program; System parameter number to change using G10", - 'clean_value': CLEAN_INT, - }, - # Program Name - 'O': { - 'class': str, - 'value_regex': re.compile(r'^.+$'), # all the way to the end - 'description': "Program name", - 'clean_value': CLEAN_NONE, - }, - # Parameter (arbitrary parameter) - 'P': { - 'class': float, # parameter is often an integer, but can be a float - 'value_regex': REGEX_FLOAT, - 'description': "Serves as parameter address for various G and M codes", - 'clean_value': CLEAN_FLOAT, - }, - # Peck increment - 'Q': { - 'class': float, - 'value_regex': REGEX_FLOAT, - 'description': "Depth to increase on each peck; Peck increment in canned cycles", - 'clean_value': CLEAN_FLOAT, - }, - # Arc Radius - 'R': { - 'class': float, - 'value_regex': REGEX_FLOAT, - 'description': "Defines size of arc radius, or defines retract height in milling canned cycles", - 'clean_value': CLEAN_FLOAT, - }, - # Spindle speed - 'S': { - 'class': float, - 'value_regex': REGEX_FLOAT, - 'description': "Defines speed, either spindle speed or surface speed depending on mode", - 'clean_value': CLEAN_FLOAT, - }, - # Tool Selecton - 'T': { - 'class': str, - 'value_regex': REGEX_POSITIVEINT, # tool string may have leading '0's, but is effectively an index (integer) - 'description': "Tool selection", - 'clean_value': CLEAN_NONE, - }, - # Incremental axes - 'U': { - 'class': float, - 'value_regex': REGEX_FLOAT, - 'description': "Incremental axis corresponding to X axis (typically only lathe group A controls) Also defines dwell time on some machines (instead of 'P' or 'X').", - 'clean_value': CLEAN_FLOAT, - }, - 'V': { - 'class': float, - 'value_regex': REGEX_FLOAT, - 'description': "Incremental axis corresponding to Y axis", - 'clean_value': CLEAN_FLOAT, - }, - 'W': { - 'class': float, - 'value_regex': REGEX_FLOAT, - 'description': "Incremental axis corresponding to Z axis (typically only lathe group A controls)", - 'clean_value': CLEAN_FLOAT, - }, - # Linear Axes - 'X': { - 'class': float, - 'value_regex': REGEX_FLOAT, - 'description': "Absolute or incremental position of X axis.", - 'clean_value': CLEAN_FLOAT, - }, - 'Y': { - 'class': float, - 'value_regex': REGEX_FLOAT, - 'description': "Absolute or incremental position of Y axis.", - 'clean_value': CLEAN_FLOAT, - }, - 'Z': { - 'class': float, - 'value_regex': REGEX_FLOAT, - 'description': "Absolute or incremental position of Z axis.", - 'clean_value': CLEAN_FLOAT, - }, -} - - class Word(object): - def __init__(self, *args): - if len(args) not in (1, 2): - raise AssertionError("input arguments either: (letter, value) or (word_str)") - if len(args) == 2: - # Word('G', 90) - (letter, value) = args - else: + def __init__(self, *args, **kwargs): + # Parameters (listed) + args_count = len(args) + if args_count == 1: # Word('G90') letter = args[0][0] # first letter value = args[0][1:] # rest of string + elif args_count == 2: + # Word('G', 90) + (letter, value) = args + else: + raise AssertionError("input arguments either: (letter, value) or (word_str)") + + # Parameters (keyword) + dialect = kwargs.pop('dialect', dialects.get_default()) + letter = letter.upper() - self._value_class = WORD_MAP[letter]['class'] - self._value_clean = WORD_MAP[letter]['clean_value'] + self._word_map = getattr(getattr(dialects, dialect), 'WORD_MAP') + self._value_class = self._word_map[letter].cls + self._value_clean = self._word_map[letter].clean_value self.letter = letter self.value = value @@ -272,15 +85,19 @@ class Word(object): @property def description(self): - return "%s: %s" % (self.letter, WORD_MAP[self.letter]['description']) + return "%s: %s" % (self.letter, self._word_map[self.letter].description) -def text2words(block_text): +def text2words(block_text, dialect=None): """ Iterate through block text yielding Word instances :param block_text: text for given block with comments removed """ - next_word = re.compile(r'^.*?(?P[%s])' % ''.join(WORD_MAP.keys()), re.IGNORECASE) + if dialect is None: + dialect = dialects.get_default() + word_map = getattr(getattr(dialects, dialect), 'WORD_MAP') + + next_word = re.compile(r'^.*?(?P[%s])' % ''.join(word_map.keys()), re.IGNORECASE) index = 0 while True: @@ -291,7 +108,7 @@ def text2words(block_text): index += letter_match.end() # propogate index to start of value # Value - value_regex = WORD_MAP[letter]['value_regex'] + value_regex = word_map[letter].value_regex value_match = value_regex.search(block_text[index:]) if value_match is None: raise GCodeWordStrError("word '%s' value invalid" % letter) diff --git a/tests/test_words.py b/tests/test_words.py index 5c98a2c..e197cbd 100644 --- a/tests/test_words.py +++ b/tests/test_words.py @@ -6,6 +6,7 @@ add_pygcode_to_path() # Units under test from pygcode import words +from pygcode import dialects class WordIterTests(unittest.TestCase): @@ -33,8 +34,7 @@ class WordIterTests(unittest.TestCase): self.assertEqual([w[5].letter, w[5].value], ['F', 70]) -class WordValueMatchTests(unittest.TestCase): - +class WordValueMatchTest(unittest.TestCase): def regex_assertions(self, regex, positive_list, negative_list): # Assert all elements of positive_list match regex for (value_str, expected_match) in positive_list: @@ -47,9 +47,11 @@ class WordValueMatchTests(unittest.TestCase): match = regex.search(value_str) self.assertIsNone(match, "matched for '%s'" % value_str) + +class WordTests_LinuxCNC(WordValueMatchTest): def test_float(self): self.regex_assertions( - regex=words.REGEX_FLOAT, + regex=dialects.linuxcnc.REGEX_FLOAT, positive_list=[ ('1.2', '1.2'), ('1', '1'), ('200', '200'), ('0092', '0092'), ('1.', '1.'), ('.2', '.2'), ('-1.234', '-1.234'), @@ -63,7 +65,7 @@ class WordValueMatchTests(unittest.TestCase): def test_code(self): self.regex_assertions( - regex=words.REGEX_CODE, + regex=dialects.linuxcnc.REGEX_CODE, positive_list=[ ('1.2', '1.2'), ('1', '1'), ('10', '10'), ('02', '02'), ('02.3', '02.3'),